Getting started
Foundations
Integrations
Form19
Display5
Layout4
Navigation4
Overlay8
Feedback4
Motion19
Streaming Text
Animated
Text revealed a word at a time as it streams in, without rewinding.
#Installation
npx @dinachi/cli@latest add streaming-text#Usage
tsx
import { StreamingText } from "@/components/ui/streaming-text"tsx
<StreamingText text={answer} complete={!pending} onDone={() => setDone(true)} />#Examples
#complete tells it the stream ended
The component cannot work that out for itself. Catching up to the current string and the
stream finishing are indistinguishable from inside: between two chunks they look identical.
Only the caller knows which one happened, so complete is what drives the blinking caret,
onDone, and the announcement.
tsx
const [text, setText] = useState("")
const [pending, setPending] = useState(true)
<StreamingText text={text} complete={!pending} />Leave it at its default of true for a string that is already whole.
#What it does differently
| Why | |
|---|---|
| Paced by elapsed time | A dropped frame costs smoothness, not sync. A per-character timer drifts against the stream. |
| Reveals whole words | Per-character reveal reflows the line on nearly every frame: expensive, and unreadable as words break and rejoin. |
| Append-aware | Progress only moves forward. Text arriving late raises the ceiling rather than rewinding the reader to the first word. |
#When to use it
| Use it for | Not for |
|---|---|
| Text that is genuinely arriving over a network, a token at a time. | A string you already have in full. The reveal is a claim about where the text is coming from, and playing it over finished content costs the reader the seconds it takes to run. |
| A single answer the reader is waiting on. | A page of body copy. Nobody wants to watch an article type itself. |
#Behaviour
pausedstops the clock, not just the render. Resuming does not dump the banked time out in one frame.runKeyrewinds to the first word. It is the only way back, since appending never restarts.wordsPerSecondcan change mid-stream. The pace is read inside the frame loop rather than closed over it, so a change of pace is not a change of content.
#Accessibility
- The visible paragraph is
aria-hidden. A live region over a growing paragraph re-announces the whole thing on every word. - The finished text is announced once, through a polite live region, when
completebecomes true. - Reduced motion keeps the arrival and drops the blur-in. The streaming is the data, not the decoration.
- The caret only blinks once the stream stops, where it means "your turn". A blink during output competes with the words for attention.
#API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| text | string | — | The text so far. May grow between renders; appending resumes rather than restarts |
| complete | boolean | true | Whether the stream has ended. The component cannot infer it — catching up is not the same event as finishing |
| runKey | number | 0 | Change it to rewind to the first word |
| paused | boolean | false | Holds the reveal where it is. The clock stops with it, so there is no catch-up burst on resume |
| wordsPerSecond | number | 14 | Reveal pace. Changing it mid-stream changes the pace without restarting |
| onDone | () => void | — | Fired once the reveal has caught up and complete is true |