Streaming text
Text arriving as it's written: words fading in, markdown that forms cleanly mid-stream, a soft-focus edge, a caret with stop and skip controls, and a table filling row by row.
Loading preview…
Install
- 1
Run this command in your Tailwind CSS project. It adds
components/ai/streaming-text-default.tsxand installs its dependencies.$ npx shadcn@latest add https://gradientdeck.com/ai-components/r/streaming-text-default.json
Build a streaming chat interface in React →
Usage & API
Example
import { StreamingText } from "@/components/ai/streaming-text-default"
<StreamingText
text="…"
shown={1}
done={true}
/>Props StreamingText
| Prop | Type & description | Required |
|---|---|---|
text | string | Required |
shown | numberHow many words are visible so far. | Required |
done | boolean | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Default exports StreamingText. Words already counted in shown are visible. A whitespace token is included only when the word count has passed it, so you do not get a leading gap. The caret is aria-hidden and blinks on a 0.9 second loop. done removes it. There is no stop button and no speed. You stop by freezing shown and setting done, or by leaving done false and simply not incrementing. The demo's reply is a paragraph about tides, and a replay control lives outside the component. Reduced motion skips the fade and still shows the words. aria-busy is true until done.
Example
import { StreamingMarkdown } from "@/components/ai/streaming-text-markdown"
<StreamingMarkdown
source="…"
complete={true}
/>Props StreamingMarkdown
| Prop | Type & description | Required |
|---|---|---|
source | string | Required |
complete | boolean | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Markdown exports StreamingMarkdown and parseMarkdown. source is the raw string so far. complete tells the parser the last line is finished. Headings of one to three hashes become an h3 style, not a real h1, so a stream cannot dump a page-level heading. Lists are lines that start with dash space. A fence opens a code block as soon as the backticks arrive. The language label stays blank until that opening line is complete. Copy inside the fence is disabled while the fence is still open, and it writes the joined lines. Highlighting colors comments, strings, numbers and a short list of keywords such as import, def and return. Indentation is padding, because the font is proportional. A trailing incomplete marker is dropped rather than shown as a stray hash or dash. aria-busy follows complete. The demo's Copy reply and Regenerate buttons sit outside StreamingMarkdown.
Example
import { SoftFocusText } from "@/components/ai/streaming-text-blur"
<SoftFocusText
paragraphs={[]}
shown={1}
done={true}
/>Props SoftFocusText
| Prop | Type & description | Required |
|---|---|---|
paragraphs | string[]Paragraphs of the full reply. | Required |
shown | numberWords shown so far, counted across paragraphs. | Required |
done | boolean | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Soft focus exports SoftFocusText. paragraphs is the full reply, split by you. shown counts words from the start of the first paragraph across the rest. A paragraph with no visible words yet renders nothing, so you do not get an empty p. Each word is an inline-block that animates from opacity 0 and blur 8px to fully sharp, over 0.75 seconds. Reduced motion uses a 0.2 second opacity fade and no blur. aria-busy is true until done. The demo reveals one word at a time on a repeating list of gaps, 70, 55, 110 and so on, so the edge does not tick like a metronome. Copy reply and Regenerate in that demo are siblings, with labels that switch to Copied. They are not props of SoftFocusText.
Example
import { CaretStream } from "@/components/ai/streaming-text-caret"
<CaretStream
text="…"
shown={1}
state={state}
/>Props CaretStream
| Prop | Type & description | Required |
|---|---|---|
text | string | Required |
shown | numberCharacters shown so far. | Required |
state | StreamState | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Caret and controls exports CaretStream, ReplyControls, and the StreamState and StreamSpeed types. The caret is a thin indigo bar, hidden once state is done, and the paragraph is aria-busy only while streaming. Stopped keeps the caret, which is how a paused reply still looks unfinished. ReplyControls swaps its cluster. While streaming you get Stop and Skip to end. While stopped you get a status that reads Stopped, plus Continue and Skip to end. While done you get Copy or Copied, and Regenerate. The speed radio group is labelled Speed and is hidden once done. Slow, Normal and Fast call onSpeedChange. Arrow keys move between them. The demo measures elapsed time and adds characters at the chosen cps, so a speed change takes effect on the next frame without restarting the reply. Skip sets the position to the end. Stop freezes it.
Example
import { StreamingTable } from "@/components/ai/streaming-text-table"
<StreamingTable
columns={[]}
rows={[]}
header={true}
cells={1}
chars={1}
/>Props StreamingTable
| Prop | Type & description | Required |
|---|---|---|
columns | string[] | Required |
rows | string[][]Each row's first cell is its label. | Required |
header | booleanWhether the header row has arrived. | Required |
cells | numberCells fully written, counted left to right, top to bottom. | Required |
chars | numberCharacters written of the next cell. | Required |
Dependencies
- React
- Tailwind CSS
- motion
- lucide-react
Implementation notes
Table rows exports StreamingTable. columns are header strings. Each row is a string array, and the first cell is the row label. header gates the thead, so you can wait until the header row has arrived. cells is how many cells are fully written, counted left to right, top to bottom. chars is how many characters of the next cell to show. A cell that is not there yet renders a shimmering rounded bar, unless reduced motion is on, in which case the bar is still. The table's aria-busy stays true until cells reaches rows times columns. The first column is a row header. Later cells are tabular numbers. The demo writes the London trip comparison, then offers Copy reply and Regenerate outside the table. The component does not format currency or units. The strings you pass are the strings it shows, including the approximation marks in the CO2 row.
Choosing a layout
Use Default for prose that should feel like typing without being a terminal. Use Markdown when the model streams headings, bullets and fences and you cannot wait until the end to parse, because a half-written hash would otherwise flash as a paragraph. Use Soft focus when the leading edge should look soft rather than carry a caret. The demo is two paragraphs on why bread goes stale, revealed on an uneven gap pattern. Use Caret and controls when the person must stop, resume, skip to the end, or change speed before the reply finishes. Use Table rows when the answer is a comparison grid. The demo columns are a blank corner, LNER train, Flight and Caledonian Sleeper, and the rows cover door to door time, fare, CO2, luggage and working on board.
Message is the layout once you have a finished node. Text shimmer is the status before the first token. Message error replaces the stream when it fails, and its stopped variant is a smaller notice, Stopped by you, plus Continue. The caret controls already say Stopped and offer Continue and Skip to end, so you rarely need both on the same reply. Copy on the markdown, blur and table demos is labelled Copy reply. The caret's copy button says Copy, then Copied. Message actions is the richer row, with thumbs, for after done is true.
shown means words in one variant and characters in another. Which is it?
Default and Soft focus count words. Caret counts characters, and the demo moves that count at 26, 60 or 150 characters per second depending on Slow, Normal or Fast. Table does not use shown. It uses cells for finished cells and chars for the cell being typed. Markdown does not use a count either. You append to source and flip complete when the stream ends. Passing a word count into CaretStream will reveal only that many characters.
A markdown heading flashes as a hash and then jumps into a title.
That happens if you render the raw source yourself. StreamingMarkdown drops a final line that is only #, ##, ###, - or a short backtick run, until more text arrives. A finished heading line becomes a heading. If complete is true, the last line is not treated as a tail, so a real heading at the end is kept. Fences open the chrome early on purpose, with an empty language until the opening line finishes, and Copy stays disabled while open is true so a half-written block is not copied.