Streaming Text

Animated

Text revealed a word at a time as it streams in, without rewinding.

View Source

#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 timeA dropped frame costs smoothness, not sync. A per-character timer drifts against the stream.
Reveals whole wordsPer-character reveal reflows the line on nearly every frame: expensive, and unreadable as words break and rejoin.
Append-awareProgress only moves forward. Text arriving late raises the ceiling rather than rewinding the reader to the first word.

#When to use it

Use it forNot 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

  • paused stops the clock, not just the render. Resuming does not dump the banked time out in one frame.
  • runKey rewinds to the first word. It is the only way back, since appending never restarts.
  • wordsPerSecond can 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 complete becomes 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

PropTypeDefaultDescription
textstringThe text so far. May grow between renders; appending resumes rather than restarts
completebooleantrueWhether the stream has ended. The component cannot infer it — catching up is not the same event as finishing
runKeynumber0Change it to rewind to the first word
pausedbooleanfalseHolds the reveal where it is. The clock stops with it, so there is no catch-up burst on resume
wordsPerSecondnumber14Reveal pace. Changing it mid-stream changes the pace without restarting
onDone() => voidFired once the reveal has caught up and complete is true