Streaming Text

Words resolve into a paragraph one at a time, through a small blur.

Loading preview

Installation

npx shadcn@latest add @loomui/streaming-text

Usage

import { StreamingText } from "@/components/ui/streaming-text"
<StreamingText active={isStreaming} text="The full paragraph, all at once." />

How it works

The text is split into words up front rather than typed character by character — every word is already in the DOM, resting at opacity: 0 and blurred. Turning active on doesn't fade them all in together: each word's data-in attribute flips on its own setTimeout, staggered by wordGap, so the paragraph resolves left to right the way a response actually streams in.

Replaying is the same trick ThinkingText uses for its state swap: wipe every word back to hidden with data-resetting (which forces transition: none), let two animation frames pass so the browser commits that as a real paint rather than getting collapsed into the next update, then release the reset and start staggering data-in back on. Without that forced frame, the "hidden" state and the first reveal would land in the same paint and nothing would visibly reset.

Replaying on purpose

Toggling active off and back on replays the whole paragraph from scratch — that's deliberate, not a bug. If text changes while active is already true, it replays too. For streamed content, wait for the full string before mounting with active, or swap in the whole component with a key change when you want a clean rerun, the way the demo does it.

Props

PropTypeDefaultDescription
activebooleanfalseRuns the reveal. Off shows the text at once.
textstring-The full paragraph, split on whitespace.
wordGapnumber45Milliseconds between each word's reveal.
fadeDurationnumber320Milliseconds each word's fade-in takes.
classNamestring-Merged onto the wrapper.

Any other <span> prop is forwarded.

Reduced motion

Every word renders at full opacity with no blur and no transition — the paragraph is just there, in one paint, with nothing to stagger through.