Typewriter
Types and deletes phrases with a blinking caret.
Ask anything
components/ui/typewriter.tsx
"use client";
import { motion } from "motion/react";
import { useEffect, useState } from "react";
type TypewriterProps = {
phrases: string[];
typingSpeed?: number;
deletingSpeed?: number;
/** Milliseconds to hold a finished phrase */
pause?: number;
className?: string;
};
export function Typewriter({
phrases,
typingSpeed = 55,
deletingSpeed = 30,
pause = 1500,
className = "",
}: TypewriterProps) {
const [phraseIndex, setPhraseIndex] = useState(0);
const [text, setText] = useState("");
const [deleting, setDeleting] = useState(false);
useEffect(() => {
const full = phrases[phraseIndex % phrases.length];
let delay = deleting ? deletingSpeed : typingSpeed;
if (!deleting && text === full) delay = pause;
const timer = setTimeout(() => {
if (!deleting && text === full) return setDeleting(true);
if (deleting && text === "") {
setDeleting(false);
return setPhraseIndex((i) => i + 1);
}
setText(deleting ? full.slice(0, text.length - 1) : full.slice(0, text.length + 1));
}, delay);
return () => clearTimeout(timer);
}, [text, deleting, phraseIndex, phrases, typingSpeed, deletingSpeed, pause]);
return (
<span className={className}>
{text}
<motion.span
aria-hidden
className="ml-0.5 inline-block h-[1em] w-[2px] translate-y-[0.12em] bg-current"
animate={{ opacity: [1, 0, 1] }}
transition={{ duration: 1, repeat: Infinity, ease: "linear" }}
/>
</span>
);
}
export default function Demo() {
return (
<div className="text-center">
<p className="text-sm font-medium uppercase tracking-widest text-zinc-400">Ask anything</p>
<p className="mt-3 text-3xl font-semibold tracking-tight text-zinc-900 sm:text-4xl">
<Typewriter
phrases={["Summarize this quarter's revenue", "Draft a launch announcement", "Find churn risks in Q3"]}
/>
</p>
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/typewriter.tsxcomponents/ui/typewriter.tsx"use client"; import { motion } from "motion/react"; import { useEffect, useState } from "react"; type TypewriterProps = { phrases: string[]; typingSpeed?: number; deletingSpeed?: number; /** Milliseconds to hold a finished phrase */ pause?: number; className?: string; }; export function Typewriter({ phrases, typingSpeed = 55, deletingSpeed = 30, pause = 1500, className = "", }: TypewriterProps) { const [phraseIndex, setPhraseIndex] = useState(0); const [text, setText] = useState(""); const [deleting, setDeleting] = useState(false); useEffect(() => { const full = phrases[phraseIndex % phrases.length]; let delay = deleting ? deletingSpeed : typingSpeed; if (!deleting && text === full) delay = pause; const timer = setTimeout(() => { if (!deleting && text === full) return setDeleting(true); if (deleting && text === "") { setDeleting(false); return setPhraseIndex((i) => i + 1); } setText(deleting ? full.slice(0, text.length - 1) : full.slice(0, text.length + 1)); }, delay); return () => clearTimeout(timer); }, [text, deleting, phraseIndex, phrases, typingSpeed, deletingSpeed, pause]); return ( <span className={className}> {text} <motion.span aria-hidden className="ml-0.5 inline-block h-[1em] w-[2px] translate-y-[0.12em] bg-current" animate={{ opacity: [1, 0, 1] }} transition={{ duration: 1, repeat: Infinity, ease: "linear" }} /> </span> ); } export default function Demo() { return ( <div className="text-center"> <p className="text-sm font-medium uppercase tracking-widest text-zinc-400">Ask anything</p> <p className="mt-3 text-3xl font-semibold tracking-tight text-zinc-900 sm:text-4xl"> <Typewriter phrases={["Summarize this quarter's revenue", "Draft a launch announcement", "Find churn risks in Q3"]} /> </p> </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.