Streamdown is designed from the ground up for streaming. When an AI model sends tokens one at a time, Streamdown parses, memoizes, and renders them efficiently without blocking the browser UI.
How streaming mode works
Streamdown operates in mode="streaming" by default. In this mode, the component:
- Preprocesses the raw markdown string through the
remend engine to complete any unterminated syntax
- Splits the result into independent blocks using
parseMarkdownIntoBlocks
- Wraps block updates in a React Transition so they never block higher-priority user interactions
- Memoizes each block so only the last (incomplete) block re-renders on each token arrival
import { Streamdown } from "streamdown";
export default function ChatMessage({ text, isStreaming }) {
return (
<Streamdown isAnimating={isStreaming}>
{text}
</Streamdown>
);
}
Block-based parsing
The parseMarkdownIntoBlocks function splits a markdown string into top-level blocks — paragraphs, headings, code fences, tables, and HTML blocks — using the marked lexer. Each block is rendered as a separate memoized Block component.
This means that when a new token arrives, only the last block (which is still being written) triggers a re-render. All previous completed blocks stay memoized and untouched.
import { parseMarkdownIntoBlocks } from "streamdown";
const blocks = parseMarkdownIntoBlocks("# Hello\n\nWorld");
// ["# Hello\n", "\nWorld"]
You can provide your own block parser via the parseMarkdownIntoBlocksFn prop:
<Streamdown parseMarkdownIntoBlocksFn={myCustomParser}>
{markdown}
</Streamdown>
When the markdown contains footnote references ([^1]) or definitions ([^1]:), the entire document is returned as a single block. This keeps footnote anchors and their definitions in the same MDAST tree so they link correctly.
React Transitions for non-blocking updates
Block updates in streaming mode are wrapped with startTransition. This marks the state update as non-urgent, so React can interrupt it to handle user input (clicks, typing) without the UI freezing.
When the animated prop is active, updates bypass the transition and apply synchronously so that the animate plugin can track character counts across renders.
parseIncompleteMarkdown prop
The parseIncompleteMarkdown prop (default: true) controls whether the remend engine runs before block parsing. When enabled, remend closes any uncompleted Markdown syntax — bold, italic, inline code, links, and list items — so that partial tokens render correctly mid-stream.
// Disable remend preprocessing (useful if your upstream already cleans the input)
<Streamdown parseIncompleteMarkdown={false}>
{markdown}
</Streamdown>
parseIncompleteMarkdown only takes effect in mode="streaming". In static mode the value is ignored and remend never runs.
normalizeHtmlIndentation prop
The normalizeHtmlIndentation prop (default: false) strips excess indentation from HTML blocks before they reach the Markdown parser. Without this, indented HTML tags can be misinterpreted as code blocks (Markdown treats 4+ leading spaces as indented code).
This is useful when rendering AI-generated HTML with nested tags indented for readability:
<Streamdown normalizeHtmlIndentation>
{aiGeneratedHtml}
</Streamdown>
The helper function is also exported for use outside the component:
import { normalizeHtmlIndentation } from "streamdown";
const cleaned = normalizeHtmlIndentation(rawHtml);
Static mode
Set mode="static" to skip all streaming-specific logic. The entire markdown string is rendered in a single pass without block splitting, transitions, or the remend engine.
<Streamdown mode="static">
{completedMarkdown}
</Streamdown>
Use static mode when:
- The content is already fully generated (chat history, pre-rendered messages)
- You need a simpler rendering path with lower overhead
- You want to avoid the flash that can occur when transitioning from streaming to done
In static mode, onAnimationStart and onAnimationEnd callbacks are suppressed even if you pass isAnimating={true}.
isAnimating prop
The isAnimating prop signals whether new tokens are actively arriving. It controls several behaviors:
- Enables or disables the character animation pipeline (when
animated is set)
- Disables copy/download buttons on code blocks and tables to prevent copying incomplete content
- Triggers
onAnimationStart / onAnimationEnd callbacks on transitions
- Suppresses the cursor caret when the last block contains an incomplete code fence or table
import { useChat } from "@ai-sdk/react";
import { Streamdown } from "streamdown";
export default function Chat() {
const { messages, status } = useChat();
return (
<div>
{messages.map((message) => (
<Streamdown key={message.id} isAnimating={status === "streaming"}>
{message.content}
</Streamdown>
))}
</div>
);
}
Always set isAnimating={false} (or omit it) for completed messages. Leaving it true permanently disables copy buttons and keeps the animation pipeline active unnecessarily.