Text
Typewriter
A type-and-delete headline that cycles through a list of sentences with a blinking caret.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/typewriter.jsonPrompt
The prompt behind this component — paste it into your AI assistant to recreate or adapt it.
Build a React + TypeScript + Tailwind "Typewriter" component (no animation
library — one setTimeout drives everything).
Contract
- Export a forwardRef span extending React.HTMLAttributes<HTMLSpanElement>.
- Props: sequences (string[], typed one after another), typeSpeed (ms per
character while typing, default 65), deleteSpeed (ms per character while
deleting, default 35), pauseMs (how long a finished sentence is held,
default 1600), loop (default true; false = stop on the last sentence and
never delete it), cursor (default true), cursorChar (default "|"),
onSequenceComplete?(index) — fired the moment a sentence's last character
lands, not when its hold ends.
- className lands on the outer span, so font size / weight / color are
inherited from the call site; the component never sets typography itself.
Behavior
- One state object holds { items, index, count, phase } where phase is
"typing" | "deleting" and count is how many characters of the current
sentence are visible. Keep a snapshot of the sequences array inside that
state so a parent re-render with a fresh array literal does not restart
the timer; reset the snapshot during render (compare a joined key, no
effect) when the sentence list actually changes.
- Exactly one setTimeout is scheduled per state, always cleared in the
effect cleanup — there is never more than one live timer:
typing & count < len -> +1 char after typeSpeed
typing & count = len -> hold pauseMs, then switch to deleting
(if loop is false and this is the last sentence,
schedule nothing: the finished sentence is the
final frame)
deleting & count > 0 -> -1 char after deleteSpeed
deleting & count = 0 -> advance index modulo sequences.length, type again
- onSequenceComplete fires inside the timeout callback that lands the final
character, so a StrictMode double-invoke is cancelled by the cleanup and
the callback never double-fires. Keep the consumer's function in a ref and
refresh it in an effect, so passing an inline arrow never restarts timers.
- prefers-reduced-motion (useSyncExternalStore over matchMedia, server
snapshot false): render the FIRST sentence in full, start no timers at
all, and keep the caret solid instead of blinking — the message is still
delivered, the motion is not.
Rendering & styling
- Semantic tokens only; the component contributes no color of its own —
it inherits currentColor, so bg-muted / text-muted-foreground wrappers
and dark mode work for free.
- Accessibility: character-by-character output is noise for screen readers,
so the animated span is aria-hidden and a sr-only span carries every
sequence joined into one static string — assistive tech reads the full
message once instead of tracking a moving target.
- The animated span uses whitespace-pre-wrap so mid-sentence and trailing
spaces are not collapsed and the caret does not jump a space backwards.
- The caret is a separate aria-hidden span animated by a hoisted @keyframes
(React 19 <style href precedence>, dedupes by href, no Tailwind config
edits) using step-end timing for a hard terminal blink, plus
motion-reduce:[animation:none].
Customization levers
- Rhythm: typeSpeed / deleteSpeed / pauseMs are independent — human typing
reads well around 60-90ms typing, 25-40ms deleting, 1.2-2s hold.
- Caret: cursorChar takes any glyph ("|", "▌", "_"), cursor={false} removes
it; swap the blink for a solid caret by dropping the animation class.
- Ending: loop={false} freezes on the last sentence — the idiomatic way to
type one headline in and leave it on screen.
- Layout stability: the line grows and shrinks as it types; wrap the
component in a fixed-height (or min-w) container when it sits above other
content you do not want reflowing.
- Chaining: use onSequenceComplete(index) to drive anything downstream —
swap a hero image per sentence, count completed lines, fire analytics.Concepts
- Type–delete loop — the sentence list is a cycle, not a playlist: every sentence is typed, held, erased, and the index wraps, so one line of copy can carry three or four messages without extra layout.
- Single-timer state machine — each state schedules exactly one
setTimeoutand the effect cleanup clears it, so unmounting mid-sentence leaks nothing and speed props take effect on the very next character. - Snapshot the sequence list — the array lives inside component state, so a parent re-render passing a new array literal cannot restart the animation; only a real content change resets it (compared during render, no effect).
- Completion is a character event, not a timer event —
onSequenceCompletefires the moment the last character lands, inside the async callback, which also makes it StrictMode-safe (a double-invoked effect is cancelled by its cleanup). - Solid caret under reduced motion — the caret keeps its shape but stops blinking and the first sentence is shown whole: the "someone is typing" signal survives without any movement.
- Static text for screen readers — the animated layer is
aria-hiddenand ansr-onlyspan holds every sequence joined, so assistive tech hears the full message once instead of a per-character stream.