Typewriter
Reveals text one token at a time at a steady cadence. Streaming-aware — grow the text prop as tokens arrive and the reveal continues from where it left off. Compose with Typewriter.Text and Typewriter.Cursor, or drive your own UI with the render-prop state.
Features
mode.text and the reveal continues from where it left off.keepMounted).displayed, isTyping, isDone, and progress.prefers-reduced-motion — reveals everything at once.Example
Assistant
Anatomy
import { Typewriter } from '@wire-ui/react'
<Typewriter.Root text="Composing a reply…" speed={45}>
<Typewriter.Text />
<Typewriter.Cursor keepMounted>▋</Typewriter.Cursor>
</Typewriter.Root>Render-prop state
Pass a function as the child of Typewriter.Root to drive your own UI from the reveal state instead of using Typewriter.Text/Typewriter.Cursor.
<Typewriter.Root text="Drive your own UI from the reveal state." speed={35}>
{({ displayed, progress, isDone }) => (
<div>
<p className="font-mono text-sm text-black">{displayed}</p>
<div className="h-1 w-full overflow-hidden rounded-full bg-[#e5e7eb]">
<div className="h-full bg-black" style={{ width: `${progress * 100}%` }} />
</div>
<p className="text-xs text-[#6b7280]">{isDone ? 'Done' : `${Math.round(progress * 100)}%`}</p>
</div>
)}
</Typewriter.Root>When no children are provided, Typewriter.Root renders the revealed text directly.
Streaming
Leave resetOnTextChange at its default (false) and grow the text prop incrementally as tokens arrive. The reveal continues from where it left off rather than restarting, smoothing out a bursty network stream.
const [text, setText] = useState('')
// append chunks as they stream in: setText((t) => t + chunk)
<Typewriter.Root text={text} speed={20}>
<Typewriter.Text />
<Typewriter.Cursor>▋</Typewriter.Cursor>
</Typewriter.Root>Styling
Both Typewriter.Text and Typewriter.Cursor (and Root) expose data-state with the value "typing" or "done". Use it to fade or hide the cursor when typing finishes, or to style the text while in progress.
<Typewriter.Cursor className="
data-[state=typing]:opacity-100
data-[state=done]:opacity-0
">
▋
</Typewriter.Cursor>A blinking cursor is purely CSS:
@keyframes blink { 0%, 49% { opacity: 1 } 50%, 100% { opacity: 0 } }
.cursor { animation: blink 1s step-end infinite }Using data attributes
/* Style text while typing */
[data-state="typing"] { /* in progress */ }
[data-state="done"] { /* finished */ }Root props
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | required | Full text to reveal. Grow it over time for streaming. |
speed | number | 30 | Milliseconds per revealed token (char or word). |
mode | 'char' | 'word' | 'char' | Reveal one character or one whole word per tick. |
startDelay | number | 0 | Delay in ms before the first token is revealed. |
autoStart | boolean | true | Begin revealing automatically on mount. |
resetOnTextChange | boolean | false | Restart from the beginning when text changes instead of continuing. |
loop | boolean | false | Clear and retype once complete. |
loopDelay | number | 1000 | Ms to wait before looping when loop is set. |
onComplete | () => void | — | Called once all text has been revealed. |
children | ReactNode | ((state: TypewriterState) => ReactNode) | — | Compose with Text/Cursor, or a render function. Omitted renders the revealed text directly. |
Text props
Typewriter.Text renders the revealed portion of text. It accepts standard element attributes (e.g. className).
Cursor props
| Prop | Type | Default | Description |
|---|---|---|---|
keepMounted | boolean | false | Keep the cursor mounted after typing finishes. When false, it unmounts once isDone is true. |
Render-prop state (TypewriterState)
| Field | Type | Description |
|---|---|---|
displayed | string | The portion of text currently revealed. |
isTyping | boolean | True while tokens are still being revealed. |
isDone | boolean | True once all of text has been revealed. |
progress | number | Fraction revealed, 0–1. |
Data attributes
| Attribute | Element | Values |
|---|---|---|
data-state | Root, Text, Cursor | "typing" / "done" |
Accessibility
Typewriter.Rootsetsaria-busywhile typing is in progress.Typewriter.Cursoris rendered witharia-hidden="true"so it is not announced.- Respects
prefers-reduced-motion: when set, all text is revealed immediately rather than animated.