Typewriter

Free · MIT

Types and deletes a list of phrases with a human rhythm and a caret that only blinks while idle; works as an animated input placeholder.

MarkdownOpen in v0
Open in v0

Installation

$ npx shadcn@latest add https://goodui.dev/r/typewriter.json

The 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

Use it for
  • 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.
Reach for something else
  • 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 needUse
Loop through several phrasesphrases={[…]} (loop is on by default)
Type once and stopphrases="…" loop={false}
Animated input placeholderpaused={focused || value !== ""}, positioned over the input
Slower, more deliberate typingtypeSpeed={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

PropTypeDefault
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).
number55
deleteSpeed
Milliseconds per deleted character.
number28
pause
Milliseconds a finished phrase stays on screen.
number1600
startDelay
Milliseconds before the first character.
number300
loop
Start again from the first phrase after the last one.
booleantrue
caret
Show the blinking caret.
booleantrue
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.
booleanfalse
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