Markdown
Streaming Markdown for assistant answers. Unfinished syntax heals as it arrives, finished blocks never re-render, and code, tables, math and diagrams hold their space from the first character.
On this page
Installation
Usage
import { Markdown } from "@/components/ui/chatcn/markdown"
<Markdown streaming={status === "streaming"}>{part.text}</Markdown>Pass the text so far as children and set streaming while more may arrive. Markdown is built on Streamdown, so it splits the answer into blocks and only re-parses the last one: a 3,000 word answer stays at 60 fps.
Examples
Every element
Prose is set at 15/24. Headings are 600 weight with tightening tracking, quotes carry a 1 px rule, links underline with an offset, and code, tables and images use the chatcn components.
Cadence
Tokens arrive in bursts. cadence buffers them and releases a steady flow, speeding up with the backlog so it never falls more than about 240 ms behind. It reveals whole words, never animates characters, and turns off under reduced motion.
Math and diagrams
$$…$$, \[…\] and \(…\) render with KaTeX, and mermaid fences render as diagrams. Neither library loads until an answer uses it. While a formula or diagram streams, its source holds the space.
Link safety
With linkSafety, external links open a confirmation that shows the full address, so a link labeled with one domain cannot quietly open another. onLinkCheck lets trusted URLs through.
No layout shift
- Code block chrome (header, copy, background) appears with the opening fence. Highlighting swaps in on the same text, so lines never move.
- Display math reserves the height of a one-line formula while it streams; KaTeX fonts load before the first render, so glyphs never swap.
- Finished blocks keep their props and never re-render. Measured with the Layout Instability API, the demo above streams with a cumulative layout shift under 0.001.
- Unfinished bold, links, inline code and images heal instead of flashing raw syntax. A link whose URL has not arrived reads as a link but goes nowhere.
Props
| Prop | Type | Notes |
|---|---|---|
children | string | Markdown source, the text so far while streaming |
streaming | boolean | Heals unfinished syntax, shows the caret, marks the region busy |
cadence | boolean | { charsPerSecond, maxLagMs } | Smooth streaming. Off by default |
caret | boolean | Square caret after the last line. On by default |
announce | boolean | One polite announcement when the answer completes. On by default |
linkSafety | boolean | { onLinkCheck } | Confirm before opening external links |
math | boolean | { singleDollar } | KaTeX math. singleDollar also allows $…$ |
codeBlock | { showLineNumbers, collapseAfter, defaultWrap } | Defaults for fenced code |
density | "default" | "comfortable" | 15/24 or 16/26 prose |
components | Components | Override element renderers. Pass a stable object |
Fences read title="file.ts", {3-5} and showLineNumbers from their meta string: ``` `ts title="lib/retry.ts" {4} ```.
Keyboard
| Key | Action |
|---|---|
Tab | Move through links, code block actions, table actions and diagram controls |
Arrow keys | Scroll a focused code block or table |
Enter | Open a link, or confirm in the link safety dialog |
Esc | Close the link safety dialog or an expanded table |
Accessibility
- Output is semantic HTML: real headings, lists, tables and links.
- While streaming the region is
aria-busy, and nothing is announced per token. When the answer completes, a polite live region reads it once. - External links say they open in a new tab. Links are underlined, never distinguished by color alone.
- Code blocks and tables are focusable scroll regions with labels such as TypeScript code.
Motion
- Text appends with no per-word or per-character effects.
- The caret is solid while tokens flow and blinks only after 600 ms of waiting. It is static under reduced motion.
- Cadence is pacing, not animation: under reduced motion it is off and text appears as it arrives.