Streaming Text
betaToken-by-token model output with an even reveal cadence, word-group fade/blur-in and a soft caret. Takes a growing string or an async iterable.
Async iterable
minimal
Growing string, every prop
Drive it with a growing `text` + `streaming`, tune `speed`, and react to `onComplete`.
full
0 chars · completed 0×
Custom renderer with useStreamingText
Shimmering “Thinking…” until the first token, then a hand-rolled renderer built on the hook, with a Stop button.
composition
Assistant
Thinking…
Installation
npx shadcn@latest add https://kuro.abstergo.fyi/r/streaming-text.jsonCopies the source into components/kura/streaming-text with its dependencies (kura-theme, utils). Tokens come from the kura-theme item.
When to use it
Use it for
- Render an LLM answer while it streams in (AI SDK, OpenAI/Anthropic SSE, a ReadableStream adapted to an async iterable).
- Smooth out bursty token delivery so text appears at a calm, even rhythm instead of in jumps.
- Show a live caret at the end of text that is still being generated.
- Build a custom renderer (markdown, rich text) on top of the `useStreamingText` hook exported from the same folder.
Reach for something else when
- Static text that is already complete — render it directly (StreamingText shows initial text instantly anyway).
- A “Thinking…” label before the first token — use shimmer-text (or reasoning for a collapsible trace).
- A full chat bubble with avatar and actions — use message, which composes streaming-text.
- Streaming code — use code-block with `streaming` so partial lines do not flicker.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| text | string | — | The text received so far. Pass a growing string; a change that is not an append restarts the reveal from the common prefix. |
| stream | AsyncIterable<string> | null | — | An async iterable of chunks (an async generator, AI SDK `textStream`). Takes precedence over `text`. Each iterable is consumed once and shared across re-renders. |
| speed | number | 90 | Minimum reveal cadence in characters per second. Bursts are buffered and released at this rate, faster when a backlog builds up (never more than ~½s behind). `0` disables smoothing. |
| streaming | boolean | — | More text is still coming: keeps the caret visible and `aria-busy` set. Defaults to true while `stream` is being read, otherwise false. |
| onComplete | () => void | — | Called once when the stream has ended and every character has been revealed (useful to enable message actions). |
| animation | "blur" | "fade" | "none" | "blur" | How each newly revealed word group enters: fade + 3px blur, fade only, or no animation. |
| caret | boolean | true | Show a soft pulsing caret at the live edge while streaming. It fades out (without reflow) when the stream completes. |
All native attributes of the root element, className and ref pass through.
Anatomy
- StreamingTextRoot, `white-space: pre-wrap`. `data-status="streaming" | "done"`, `aria-busy` while streaming.
data-slot="streaming-text" - ChunkA recently revealed word group, animating in. Chunks merge into one text node ~0.7s after they appear, so long answers stay a single node.
data-slot="streaming-text-chunk" - CaretDecorative caret (aria-hidden). `data-visible` reflects streaming.
data-slot="streaming-text-caret"
Accessibility
- The text is real text in the DOM; the root sets `aria-busy` while streaming so a surrounding `role="log"` / live region announces the finished answer instead of every token.
- Do not make StreamingText itself a live region — put it inside message / a `role="log"` list.
- The caret is decorative (`aria-hidden`).
- Reduced motion: Word groups only fade in (no blur) and the caret stops pulsing; the even reveal cadence is kept because it carries meaning (text is still arriving).
Notes for agents
- Pass the SAME iterable object across renders (store it in state/ref). Creating a new generator in JSX on every render restarts the stream.
- For a growing string, set `streaming` while tokens are still arriving; otherwise the caret hides and `onComplete` fires each time the reveal catches up.
- Text present at mount renders instantly — remounting or hydrating a finished message never replays the animation.
- For markdown, call `useStreamingText({ stream })` and feed `displayed` to your markdown renderer (or render `settled` + `chunks` yourself to keep the per-chunk fade).
- Import the hook from the same entry: `import { StreamingText, useStreamingText } from "@kura-ui/react/streaming-text"`.