Typewriter
Free · MITTypes and deletes a list of phrases with a human rhythm and a caret that only blinks while idle; works as an animated input placeholder.
Installation
$ npx shadcn@latest add https://goodui.dev/r/typewriter.jsonThe file lands in components/goodui/typewriter.tsx and is yours to edit. With the @goodui registry added, npx shadcn@latest add @goodui/typewriter works too.
Usage
import { Typewriter } from "@/components/goodui/typewriter"
export function Hero() {
return (
<h1 className="text-4xl font-semibold tracking-tight">
Your front desk can now{" "}
<Typewriter
className="text-primary"
phrases={["book deep cleans", "send quotes", "remind no-shows", "collect deposits"]}
/>
</h1>
)
}When to use
- A hero line that types several use cases in turn ("Book a deep clean…", "Send a quote…").
- An animated placeholder behind a search or AI prompt input, paused while the input is focused.
- Chat or assistant previews where text should appear as if typed.
- Swapping one word inside a sentence: use word-rotate, which keeps the sentence readable at all times.
- Streaming real AI output: render the streamed tokens directly; this component types fixed phrases on a timer.
- Text that must be readable immediately (prices, instructions): use text.
| If you need | Use |
|---|---|
| Loop through several phrases | phrases={[…]} (loop is on by default) |
| Type once and stop | phrases="…" loop={false} |
| Animated input placeholder | paused={focused || value !== ""}, positioned over the input |
| Slower, more deliberate typing | typeSpeed={90} pause={2400} |
Examples
Animated placeholder for an AI prompt input
const [value, setValue] = React.useState("")
const [focused, setFocused] = React.useState(false)
<div className="relative">
<input
value={value}
onChange={(e) => setValue(e.target.value)}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
aria-label="Ask the assistant"
className="h-11 w-full rounded-lg border bg-background px-3"
/>
{!value && (
<Typewriter
paused={focused}
phrases={["Move Maya's 3pm to Friday", "Who hasn't paid this month?", "Draft a quote for a 3-bed move-out"]}
className="pointer-events-none absolute inset-y-0 start-3 flex items-center text-muted-foreground"
/>
)}
</div>API reference
| Prop | Type | Default |
|---|---|---|
| phrases* One phrase or a list of phrases typed one after another. | string | string[] | — |
| typeSpeed Milliseconds per typed character (lightly randomised so it feels human). | number | 55 |
| deleteSpeed Milliseconds per deleted character. | number | 28 |
| pause Milliseconds a finished phrase stays on screen. | number | 1600 |
| startDelay Milliseconds before the first character. | number | 300 |
| loop Start again from the first phrase after the last one. | boolean | true |
| caret Show the blinking caret. | boolean | true |
| caretClassName Classes for the caret, e.g. a color or width. | string | — |
| paused Freeze typing where it is, for example while an input is focused. | boolean | false |
| onPhraseComplete Called with the phrase index each time a phrase is fully typed. | (index: number) => void | — |
Also accepts every <span> attribute.
Accessibility
- Screen readers get every phrase at once in an sr-only span; the typed characters and the caret are aria-hidden.
- When used as a placeholder, give the input its own aria-label; the typed text is decoration.
Motion
- Types one character every `typeSpeed` ms (55), randomized between 0.6x and 1.4x so it feels human; deletes at `deleteSpeed` ms (28) per character.
- A finished phrase stays for `pause` ms (1600); the first character waits `startDelay` ms (300).
- The caret blinks (1.05 s, step-end) only while idle and stays solid while typing or deleting.
- Reduced motion: whole phrases appear without typing and swap every 1.5 × pause; the caret does not blink.
Notes for AI
Coding agents read these rules through MCP and llms.txt, or paste this page with Copy page.
- The prop is phrases (a string or string[]), not text, words or strings.
- All timing props are milliseconds.
- paused freezes typing where it is; use it while an input is focused instead of unmounting.
- loop={false} stops after the last phrase stays on screen; with one phrase and loop on, it deletes and retypes the same phrase.
- It renders a <span>, so put it inside your heading or paragraph rather than wrapping them.
FAQ
Does the caret take my text color?
Yes, it is bg-current. Change it with caretClassName, for example caretClassName="bg-primary".
Can I know when a phrase finishes?
onPhraseComplete is called with the phrase index each time a phrase is fully typed.
Related
Swaps words in place with a vertical slide and blur while the box springs to each word's width, so the sentence glides instead of jumping.
Reveals a heading word by word or letter by letter with a staggered blur, fade and rise when it scrolls into view or on demand.
Text scrambles through random glyphs and resolves left to right on mount, on view, on hover or on demand; renders the real text on the server.
Morphs between strings letter by letter: shared characters spring to their new positions while the rest blur in and out.