Typewriter Animation
Animates text like a typewriter, with optional speed, cursor, and styling controls.
1. Copy the component file
Create a new file called typewriter-animation.jsx in
your reusable components folder (for example
/src/components/) and paste the following code
into it.
typewriter-animation.jsx
import { memo, useEffect, useMemo, useState } from "react";
import styles from "./typewriter-animation.module.css";
const TypewriterAnimation = (props) => {
const {
text,
words,
hideCursor,
blinkCursor = true,
cursorVariant = "line",
speed = 100,
} = props;
const [currentText, setCurrentText] = useState(text);
const [currentWordIndex, setCurrentWordIndex] = useState(0);
const [currentLetterIndex, setCurrentLetterIndex] = useState(-1);
const wordsMap = useMemo(() => {
let wordsArr = null;
if(Array.isArray(words)) {
wordsArr = words;
} else {
wordsArr = text.split(" ").filter(Boolean);
}
return wordsArr
.map((word, wordIndex, arr) => {
const temp = [{
...(typeof word === "string" ? ({
letters: word.split(""),
speed: speed,
}) : ({
letters: word.text.split(""),
...word,
}))
}];
if (wordIndex !== (arr.length - 1)) {
temp.push({
letters: [" "],
speed,
});
}
return temp;
}).flat();
}, [text, words, speed]);
const { isLastWord, isLastLetter } = useMemo(() => {
const letters = wordsMap[currentWordIndex].letters;
const isLastWord = currentWordIndex === (wordsMap.length-1);
const isLastLetter = currentLetterIndex === (letters.length-1);
return {
isLastWord,
isLastLetter,
};
}, [wordsMap, currentWordIndex, currentLetterIndex]);
useEffect(() => {
const timeoutDuration = wordsMap[currentWordIndex].speed ?? speed;
const timeout = setTimeout(() => {
if (isLastWord && isLastLetter) {
return;
} else if (isLastLetter) {
setCurrentWordIndex(currentWordIndex+ 1);
setCurrentLetterIndex(0);
} else {
setCurrentLetterIndex(currentLetterIndex + 1);
}
}, timeoutDuration);
return () => {
clearTimeout(timeout);
};
}, [wordsMap, currentWordIndex, currentLetterIndex, speed]);
const isBlinkCursor = (
blinkCursor && (
isLastLetter === -1 ||
(isLastWord && isLastLetter)
)
);
if(currentText !== text) {
setCurrentText(text);
setCurrentWordIndex(0);
setCurrentLetterIndex(-1);
}
return (
<span
aria-label={text}
className={styles["typewriter-animation"]}
>
{wordsMap.map((word, wordIndex) => (
<span
key={`word-${wordIndex}`}
aria-hidden={true}
hidden={wordIndex > currentWordIndex}
{...word.slotProps}
>
{word.letters.map((letter, letterIndex) => (
<span
key={`letter-${letterIndex}`}
aria-hidden={true}
className={styles["letter"]}
hidden={(
wordIndex === currentWordIndex &&
letterIndex > currentLetterIndex
)}
>
{letter}
</span>
))}
</span>
))}
<span
aria-hidden={true}
className={[
styles["cursor"],
styles[cursorVariant],
isBlinkCursor ? styles["blink"] : ""
].join(" ")}
hidden={hideCursor}
>
{cursorVariant === "underscore" ? "_" : <> </>}
</span>
</span>
);
};
export default memo(TypewriterAnimation); 2. Copy the CSS module file
In the same folder, create a file called typewriter-animation.module.css and paste the following CSS.
typewriter-animation.module.css
.typewriter-animation {
--cursor-color: currentcolor;
.cursor {
display: inline-block;
background: var(--cursor-color);
&.blink {
animation: cursor-blink-keyframes 1500ms ease-in-out infinite;
}
&.line {
width: 4px;
}
&.block {
width: 12px;
}
&.underscore {
background: transparent;
animation-name: cursor-underscore-blink-keyframes;
}
}
}
@keyframes cursor-blink-keyframes {
0%,
25%,
75%,
100% {
background: transparent;
}
30%,
70% {
background: var(--cursor-color);
}
}
@keyframes cursor-underscore-blink-keyframes {
0%,
25%,
75%,
100% {
color: transparent
}
30%,
70% {
color: var(--cursor-color);
}
} 3. Use the component
Now you can import and use the component anywhere in your project.
typewriter-animation-preview.jsx
import TypewriterAnimation from "@/components/text-effects/typewriter-animation/typewriter-animation";
const TypewriterAnimationPreview = () => {
return (
<h2>
<TypewriterAnimation
text="Typing the future, live."
speed={75}
/>
</h2>
)
};
export default TypewriterAnimationPreview; Cursor Types
typewriter-animation
import TypewriterAnimation from "@/components/text-effects/typewriter-animation/typewriter-animation";
const TypewriterAnimationCursorsPreview = () => {
const text = "Typing the future, live.";
return (
<div>
<h2>
<TypewriterAnimation
text={text}
cursorVariant="line"
/>
</h2>
<br />
<h2>
<TypewriterAnimation
text={text}
cursorVariant="block"
/>
</h2>
<br />
<h2>
<TypewriterAnimation
text={text}
cursorVariant="underscore"
/>
</h2>
</div>
)
};
export default TypewriterAnimationCursorsPreview; Customize word speed and style
typewriter-animation
import TypewriterAnimation from "@/components/text-effects/typewriter-animation/typewriter-animation";
const TypewriterAnimationCustomizeWordsPreview = () => {
const words = [
{
text: "Hello",
speed: 500,
slotProps: {
style: { color: "#22d6d7" }
},
},
{
text: "World",
speed: 50,
slotProps: {
style: { color: "#de287d" }
},
},
];
return (
<h2>
<TypewriterAnimation
words={words}
/>
</h2>
)
};
export default TypewriterAnimationCustomizeWordsPreview; Props
Configure the component with the following props:
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| text | string | No | — | Simple text to type. Use for basic usage instead of words. |
| words | Array<{ text: string; speed?: number; slotProps?: object }> | No | — | Advanced configuration for typing multiple entries. text defines the actual content to display, speed lets you control typing speed per word (overriding the global speed), and slotProps allows custom styling or additional props (e.g., color, className) for that specific word—giving you fine-grained visual and behavioral control. |
| blinkCursor | boolean | No | true | Enables cursor blinking animation. |
| cursorVariant | "line" | "block" | "underscore" | No | "line" | Controls the visual style of the cursor. |
| speed | number | No | 100 | Default typing speed in milliseconds (used if not overridden in words). |