The full working code is below, including the CSS
import { useEffect, useRef, useState } from "react";
import styles from "./typewritter-effect.module.scss";
const DELAY = 180; // milliseconds
const DEFAULT_TEXT = "Hello there! This is a typewriter effect animation.";
const Typewritter = () => {
const output = useRef(null);
const [timeoutExp, setTimeoutExp] = useState<null | number>(null);
const [text, setText] = useState<string>(DEFAULT_TEXT);
const hardReset = () => {
clearTimeout(timeoutExp);
setTimeoutExp(null);
setText("");
if (output.current) {
output.current.innerHTML = "";
}
};
const animate = () => {
if (!output?.current || !text) {
return null;
}
let index = 0;
function typeNextLetter() {
if (index < text.length) {
if (output?.current) {
output.current.innerHTML += text.charAt(index);
}
index++;
const timeout = setTimeout(typeNextLetter, DELAY, "myid");
setTimeoutExp(timeout);
}
}
typeNextLetter();
};
useEffect(() => {
animate();
return () => clearTimeout(timeoutExp);
}, []);
const onChange = (event) => {
setText(event?.currentTarget?.value ?? "");
};
const onSubmit = () => {
setTimeout(() => {
animate();
}, DELAY);
hardReset();
};
return (
<div className={styles.wrapper}>
<div ref={output} id="output" className={styles.neonText} />
<textarea
onChange={onChange}
className={styles.textarea}
value={text}
placeholder="Try with your own text"
rows={3}
/>
<div className={styles.actions}>
<button className={styles.button} onClick={hardReset}>
Clear
</button>
<button className={styles.button} onClick={onSubmit}>
Submit
</button>
</div>
</div>
);
};
export default Typewritter;
The CSS file in my case is named typewritter-effect.module.scss
@import url("https://fonts.googleapis.com/css?family=Sacramento&display=swap");
.wrapper {
display: flex;
flex-flow: column nowrap;
align-items: center;
justify-content: center;
background: black;
padding-top: 60px;
padding-bottom: 60px;
min-height: 70vh; /* fallback */
min-height: 70dvh; /* dynamic viewport height */
}
.neonText {
font-size: 4rem;
text-shadow: 0 0 5px #ffa500, 0 0 15px #ffa500, 0 0 20px #ffa500,
0 0 40px #ffa500, 0 0 60px #ff0000, 0 0 10px #ff8d00, 0 0 98px #ff0000;
color: #fff6a9;
font-family: "Sacramento", cursive;
text-align: center;
animation: blink 6s infinite;
max-width: 600px;
@media only screen and (max-width: 600px) {
font-size: 2.5rem;
max-width: 350px;
}
}
@keyframes blink {
20%,
24%,
55% {
text-shadow: none;
}
0%,
19%,
21%,
23%,
25%,
54%,
56%,
100% {
text-shadow: 0 0 5px #ffa500, 0 0 15px #ffa500, 0 0 20px #ffa500,
0 0 40px #ffa500, 0 0 60px #ff0000, 0 0 10px #ff8d00, 0 0 98px #ff0000;
}
}
.textarea {
margin-top: 32px;
padding: 0.4rem 1rem;
font-size: 1rem;
line-height: 1;
}
.actions {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
}
.button {
color: white;
margin: 16px;
padding: 4px 8px;
font-size: 1rem;
width: 40%;
border: 1px solid white;
}