Streamdown: Markdown That Keeps Its Cool While Tokens Pour In
Rendering Markdown is a solved problem, right up until the Markdown is not finished yet. AI chat interfaces never hand you a complete document. Instead they hand you a string that grows one token at a time, and at any given frame that string might contain an unterminated code fence, a half-typed bold phrase, a table with a header but no separator row, or a math expression missing its closing dollar sign. Feed that mess to a conventional renderer and you get flickering markup, raw asterisks flashing on screen, and content that jumps around as each chunk lands.
Streamdown is Vercel's answer to exactly this. It is a single React component that renders Markdown the way react-markdown does, but it was designed from the ground up for the token-by-token reality of large language model output. It repairs incomplete Markdown on the fly, memoizes finished blocks so they stop reflowing, and ships with syntax highlighting, math, diagrams, and security hardening already wired up. It is the renderer behind Vercel's AI Elements chat components, but it installs as a standalone package you can drop into any React app.
Why a Streaming Renderer Is a Different Animal
The trouble with streaming is not the formatting of complete blocks. It is the formatting of the incomplete ones that exist for a fraction of a second on every render. A naive renderer treats ```ts with no closing fence as "the rest of the document is code." It shows **bol as literal asterisks until the closing stars arrive. It reflows the whole conversation each time a token lands, producing the kind of layout jank that makes a polished product feel cheap.
Streamdown solves this with a preprocessor called remend that repairs unterminated Markdown before it ever reaches the renderer. It closes dangling code fences, completes half-written emphasis and links, and strips incomplete HTML tags so they do not flash on screen. On top of that, rendering is memoized at the block level, so a paragraph that finished streaming three seconds ago does not re-render every time a new token appears below it. The result is formatting that applies progressively and stays stable, rather than thrashing on every frame.
What Comes In the Box
Streamdown is deliberately batteries-included, which is a big part of why teams reach for it over assembling their own stack:
- Drop-in
react-markdownAPI — it adopted the same prop surface (components,allowedElements,disallowedElements,skipHtml), so migrating is often a one-line import change. - Streaming-aware parsing via
remendfor unterminated blocks, toggled with theparseIncompleteMarkdownprop. - GitHub Flavored Markdown — tables, task lists, and strikethrough out of the box.
- Syntax highlighting via Shiki — 200-plus languages, paired light and dark themes, plus copy and download buttons. Unknown or truncated language identifiers fall back to plain text instead of throwing.
- Math rendering with KaTeX, including inline auto-completion that turns
$formulainto$formula$while streaming. - Mermaid diagrams rendered from code blocks, with render, download, copy, fullscreen, and pan-zoom controls.
- Security-first defaults through a two-layer model of
rehype-sanitizeplusrehype-harden. - Streaming animations that reveal content per word or character, with a staggered cascade.
Getting It Into Your Project
Install the package with your package manager of choice:
npm install streamdown
yarn add streamdown
There is one setup step that trips up almost everyone, so do not skip it. Streamdown's styling lives in Tailwind utility classes inside its compiled files, and Tailwind tree-shakes away any classes it cannot find referenced. You must point Tailwind at the package with an @source directive in your global CSS:
/* app/globals.css in a standard Next.js project */
@source "../node_modules/streamdown/dist/*.js";
In a monorepo where dependencies are hoisted to the root, add more ../ segments to reach the right node_modules. Each optional plugin you install needs its own @source line too. Beyond that, the components use shadcn/ui design tokens such as --background, --foreground, and --border. If you already use shadcn/ui these exist; otherwise define a minimal :root and .dark set or the components will render without their backgrounds and borders. Forgetting the @source line or the tokens is the number one "it looks broken" complaint, so treat both as mandatory.
Your First Render
The minimal case is genuinely just a component with a string child:
import { Streamdown } from "streamdown";
export function Answer({ markdown }: { markdown: string }) {
return <Streamdown>{markdown}</Streamdown>;
}
That single line already gives you incomplete-block repair, GitHub Flavored Markdown, and the pre-styled typography. As the markdown string grows on each render, Streamdown keeps the output stable instead of flickering. If you are migrating from react-markdown, this is frequently the entire diff: change the import, and existing components overrides keep working because the prop API matches.
Wiring It Into an AI Chat
The real payoff arrives when you connect Streamdown to a live stream. With the Vercel AI SDK, useChat gives you the messages array and a status flag, and you map each text part into a <Streamdown>. The key detail is binding isAnimating to the streaming status so the reveal animation runs only while tokens are actually arriving.
import { useChat } from "@ai-sdk/react";
import { Streamdown } from "streamdown";
import { code } from "@streamdown/code";
import { math } from "@streamdown/math";
import "katex/dist/katex.min.css";
import "streamdown/styles.css";
export function Chat() {
const { messages, status } = useChat();
return (
<div>
{messages.map((message) => (
<div key={message.id}>
<strong>{message.role === "user" ? "User: " : "AI: "}</strong>
{message.parts.map((part, index) =>
part.type === "text" ? (
<Streamdown
key={index}
animated
plugins={{ code, math }}
isAnimating={status === "streaming"}
>
{part.text}
</Streamdown>
) : null
)}
</div>
))}
</div>
);
}
Notice the plugins prop. The heavier features ship as separate @streamdown/* packages so the base bundle stays lean, and you opt into only what you use. Here we pull in @streamdown/code for Shiki highlighting and @streamdown/math for KaTeX. There are also @streamdown/mermaid for diagrams and @streamdown/cjk for Chinese, Japanese, and Korean text handling. Remember each plugin you install also needs its own @source line, and the math plugin needs the KaTeX stylesheet imported as shown.
Tuning the Stream Experience
Streamdown exposes a handful of props that shape how content arrives and behaves. The streaming animation is controlled by animated and isAnimating, and a stagger value (40 milliseconds by default) sets the cascade delay between revealed tokens; set it to 0 to reveal everything at once. For code-heavy answers you can toggle lineNumbers, swap the highlight themes with shikiTheme, and rely on the automatic plain-text fallback when a model emits a language identifier that is still mid-word.
<Streamdown
animated
isAnimating={isStreaming}
stagger={30}
lineNumbers
shikiTheme={["github-light", "github-dark"]}
inlineKatex
controls={{
table: { copy: true, download: true, fullscreen: true },
code: { copy: true, download: true },
mermaid: { download: true, copy: true, fullscreen: true, panZoom: true },
}}
>
{markdown}
</Streamdown>
The inlineKatex flag is a nice streaming-specific touch: it auto-completes $x into $x$ while tokens land, so an inline formula does not render as raw text waiting for its closing delimiter. The controls object lets you fine-tune which copy, download, fullscreen, and pan-zoom affordances appear on tables, code blocks, and Mermaid diagrams. There is also a singleTilde option, on by default, that escapes stray single tildes so a temperature range like 20~25°C does not accidentally become strikethrough.
Locking Down Untrusted AI Output
This is the section to read twice if you render output from a model that users can influence. Streamdown defaults to a permissive security posture, allowing all protocols and domains, because that is convenient for trusted content. Production apps rendering untrusted LLM output should explicitly tighten the allowlists. A model can be steered by prompt injection into emitting links or images that exfiltrate data or attempt to run script, and the allowlist knobs are how you defuse that.
<Streamdown
allowedLinkPrefixes={["https://yourapp.com", "https://docs.yourapp.com"]}
allowedImagePrefixes={["https://cdn.yourapp.com"]}
allowedProtocols={["http", "https", "mailto"]}
defaultOrigin="https://yourapp.com"
>
{markdown}
</Streamdown>
Links that fall outside allowedLinkPrefixes are rewritten to your defaultOrigin rather than left intact, which neutralizes planted malicious URLs. Restricting allowedProtocols to a sane set kills javascript: and data: schemes. Constraining allowedImagePrefixes blocks base64 tracking pixels and untrusted CDNs. Underneath all of this, rehype-sanitize strips dangerous HTML attributes and elements even when you have enabled custom tags, so the hardening holds even in permissive configurations.
How It Stacks Up
The library Streamdown replaces is react-markdown, which remains enormous and general-purpose but assumes complete blocks and will not gracefully handle mid-stream fragments. With react-markdown you also assemble GitHub Flavored Markdown, highlighting, math, sanitization, and styling yourself. Parsers like markdown-it are fast and framework-agnostic but are not React components and do no streaming repair. Even markdown-to-jsx, which has a streaming mode, takes the approach of hiding incomplete structures until they complete, causing content to pop in, whereas Streamdown progressively styles the partial Markdown so it grows smoothly.
A couple of practical caveats worth keeping in mind: Streamdown has been ESM-only since version 2.2, which matters for older toolchains, and it leans on a Tailwind plus shadcn/ui setup, so non-Tailwind projects need extra work to get the styling right.
The Takeaway
Streamdown is a focused tool that does one hard thing well: it makes Markdown look right while a language model is still typing it. If you are building a chat interface, an AI writing assistant, or any surface where text streams in token by token, it spares you from writing fragile repair logic and from bolting together highlighting, math, diagrams, and sanitization by hand. Drop it in where you would have used react-markdown, add the @source line and your design tokens, opt into the plugins you need, and tighten the security allowlists for untrusted output. From there, the flickering, the broken fences, and the layout jank simply stop being your problem.