Skip to content
ContentMarkdownMenu

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.

A long assistant answer streams in with a table, code, inline code and math, and nothing above the caret moves.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/markdown.json

Usage

tsx
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.

Headings, lists, a task list, a quote, code, a table and links in one static answer.

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.

The same bursty stream, as it arrives and smoothed by the cadence buffer.

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.

A Mermaid flowchart and KaTeX formulas, both loaded only because the answer uses them.

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.

External links ask before opening and show the full address; trusted domains pass 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

PropTypeNotes
childrenstringMarkdown source, the text so far while streaming
streamingbooleanHeals unfinished syntax, shows the caret, marks the region busy
cadenceboolean | { charsPerSecond, maxLagMs }Smooth streaming. Off by default
caretbooleanSquare caret after the last line. On by default
announcebooleanOne polite announcement when the answer completes. On by default
linkSafetyboolean | { onLinkCheck }Confirm before opening external links
mathboolean | { singleDollar }KaTeX math. singleDollar also allows $…$
codeBlock{ showLineNumbers, collapseAfter, defaultWrap }Defaults for fenced code
density"default" | "comfortable"15/24 or 16/26 prose
componentsComponentsOverride 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

KeyAction
TabMove through links, code block actions, table actions and diagram controls
Arrow keysScroll a focused code block or table
EnterOpen a link, or confirm in the link safety dialog
EscClose 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.