Typewriter Animation
Animates text like a typewriter, with optional speed, cursor, and styling controls.
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; Installation
Run the command from your project root directory (the folder that contains package.json).
Terminal / Console
npx mosaicui-cli@latest text-effects/typewriter-animation 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 = false,
blinkCursor = true,
cursorVariant = "line",
speed = 100,
className,
...restProps
} = props;
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,
}) : ({
letters: word.text.split(""),
speed,
...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,
isLastWord,
isLastLetter,
speed,
]);
useEffect(() => {
setCurrentWordIndex(0);
setCurrentLetterIndex(-1);
}, [text, words, speed]);
const isBlinkCursor = (
blinkCursor && (
isLastLetter === -1 ||
(isLastWord && isLastLetter)
)
);
const srOnlyText = useMemo(() => {
if (Array.isArray(words)) {
return words.map(word => word.text).join(" ");
}
return text;
}, [text, words]);
return (
<span
{...restProps}
className={[
className,
styles["typewriter-animation"],
].join(" ")}
>
{wordsMap.map((word, wordIndex) => (
<span
{...word.slotProps}
key={`word-${wordIndex}`}
aria-hidden={true}
hidden={wordIndex > currentWordIndex}
>
{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
className={styles["sr-only"]}
>
{srOnlyText}
</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;
position: relative;
.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;
}
}
> .sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
animation: none;
}
}
@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);
}
} Examples
Discover how supported props transform components to match your needs.
Cursor Types
Typing the future, live.
Typing the future, live.
Typing the future, live.
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
Hello World
typewriter-animation
import { useMemo } from "react";
import TypewriterAnimation from "@/components/text-effects/typewriter-animation/typewriter-animation";
const TypewriterAnimationCustomizeWordsPreview = () => {
/**
* Make sure to memoize the words prop value to avoid re-renders
* React recommendation - https://react.dev/reference/react/useMemo
*/
const words = useMemo(() => ([
{
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., style, className) for that specific word—giving you fine-grained visual and behavioral control. |
| hideCursor | boolean | No | false | Hides the typing cursor when set to true. |
| 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). |
| className | string | No | — | Optional class name applied to the root container. |
| style | React.CSSProperties | No | — | Inline styles applied to the root container. |