Animates text like a typewriter, with optional speed, cursor, and styling controls.
import TypewriterAnimation from "@/registry/text-effects/typewriter-animation/typewriter-animation";
const TypewriterAnimationPreview = () => {
return (
<TypewriterAnimation
className="text-gray-900 dark:text-gray-100 text-xl overflow-hidden"
text="Typing the future, live."
/>
)
};
export default TypewriterAnimationPreview; Install the following packages before using this component.
import { useEffect, useMemo, memo } from "react";
import {motion, stagger, useAnimate} from "motion/react";
type WordProp = {
text: string;
className?: string;
style?: React.CSSProperties,
}
type WordToken = {
text: string[];
className?: string;
style?: React.CSSProperties,
};
export type TypewriterAnimationProps = {
text?: string;
words?: WordProp[],
cursor?: boolean;
blinkCursor?: boolean;
cursorVariant?: "line" | "block" | "underscore";
stagger?: number;
} & React.ComponentProps<"span">;
const TypewriterAnimation = ({
text = "",
words,
cursor = true,
blinkCursor = true,
cursorVariant = "line",
stagger: staggerVal = 0.1,
...restProps
}: TypewriterAnimationProps) => {
const [scope, animate] = useAnimate();
useEffect(() => {
const animation = animate(
".char",
{
display: "inline-block",
opacity: 1,
},
{
ease: "easeInOut",
duration: 0.01,
delay: stagger(staggerVal),
}
);
return () => {
animation.stop();
};
}, [staggerVal]);
const wordsArr = useMemo(() => {
const _words = Array.isArray(words) ? (
words
) : (
text
.split(" ")
.map((word, wordIndex, _wordsArr) => {
const temp = [{ text: word }];
if ((_wordsArr.length - 1) !== wordIndex) {
temp.push({ text: " " })
}
return temp;
})
.flat()
);
return (
_words
.reduce((acc: WordToken[], word) => {
acc.push({
...word,
text: word.text.split(""),
});
return acc;
}, [])
);
}, [text, words]);
const srOnlyText = useMemo(() => {
if (Array.isArray(words)) {
return words.map(word => word.text).join(" ");
}
return text;
}, [text, words]);
const cursorWidth = {
line: "2px",
block: "8px",
underscore: "auto",
};
const isUnderscoreCursor = (
cursorVariant === "underscore"
);
return (
<span
{...restProps}
ref={scope}
>
{wordsArr.map((word, wordIndex) => (
<span
aria-hidden={true}
key={`word-${wordIndex}`}
className={word.className}
style={{
whiteSpace: "nowrap",
wordBreak: "keep-all",
...word.style
}}
>
{word.text.map((char, charIndex) => (
<motion.span
aria-hidden={true}
key={`word-${wordIndex}-char-${charIndex}`}
style={{
opacity: 0,
display: "none",
}}
className="char"
>
{char === " " ? <> </> : char}
</motion.span>
))}
</span>
))}
{cursor && (
<motion.span
aria-hidden={true}
className="bg-[currentColor] inline-block h-[100%] ml-[1px]"
style={{
opacity: blinkCursor ? 0 : 1,
width: cursorWidth[cursorVariant],
background: isUnderscoreCursor ? "transparent" : undefined,
}}
{...(blinkCursor && ({
animate: {
opacity: 1
},
transition: {
duration: 0.5,
repeat: Infinity,
repeatType: "reverse",
}
}))}
>
{isUnderscoreCursor ? "_" : <> </>}
</motion.span>
)}
<span className="sr-only">
{srOnlyText}
</span>
</span>
)
};
export default memo(TypewriterAnimation); | Prop | Type | Required | Default | Description |
|---|---|---|---|---|
| text | string | No | — | Simple text to type. |
| cursor | boolean | No | true | Hides the typing cursor when set to false. |
| blinkCursor | boolean | No | true | Enables cursor blinking animation. |
| cursorVariant | "line" | "block" | "underscore" | No | "line" | Controls the visual style of the cursor. |
| stagger | number | No | 0.1 | Time delay between each animated unit (in seconds). |
| className | string | No | — | Optional class name applied to the root container. |
If you find this component useful, consider starring the repository on GitHub. Found a bug or have a suggestion? Open an issue to help improve it.