Word stream
A chat reply that types in word by word behind a caret, then hands back when done.
Behaviour
- Every word is rendered up front at opacity 0 and revealed by an attribute write per tick, so there is no React render per word and the paragraph never reflows.
- Spaces, tabs and newlines are kept (whitespace-pre-wrap) and unbreakable tokens such as hashes wrap (wrap-anywhere); the caret pill rides after the newest word at full opacity (outside the word's fade), takes no width so it never pushes a word onto the next line, and blinks if the stream stalls.
- Each word fades in over 150ms. The pace follows elapsed time, not timer ticks, so a reply started in a background tab catches up when you return.
- The visible words are aria-hidden and the whole text sits once in an sr-only copy, so a live region announces the reply once.
- Changing text or interval restarts the reveal (words and caret cleared at once); the interval is cleared on unmount.
- Reduced motion: the whole text shows at once in one 200ms fade with no caret (CSS, so server HTML is unchanged) and onDone fires on mount.
Props
| Prop | Type | Description |
|---|---|---|
| text | string | The full text, already known. Spaces and newlines are kept. |
| interval | numberdefault 45 | Milliseconds between words. |
| onDone | () => void | Called once when the last word is shown (on mount under reduced motion). |
| className | string | Merged onto the root <p>; set size, leading and colour here. |
Installation
npx shadcn@latest add https://purrfectui.com/r/word-stream.jsonHow to use
import { WordStream } from "@/components/ui/word-stream";
<WordStream text={reply.body} onDone={() => scrollToBottom()} />
<WordStream text={summary} interval={30} className="text-sm leading-relaxed" />Source code
Loading…
License
MIT. Copy it into your project and change it as you like.