# Streamdown
Streamdown is a React component library designed as a drop-in replacement for `react-markdown`, specifically built for AI-powered streaming applications. It handles the unique challenges that arise when Markdown content is tokenized and streamed in real-time from AI models, such as incomplete syntax, partial code blocks, and unterminated links. The library intelligently auto-completes incomplete Markdown blocks so they render correctly during streaming, providing a seamless user experience.
The monorepo includes the core `streamdown` package along with optional plugins for syntax highlighting (`@streamdown/code`), Mermaid diagrams (`@streamdown/mermaid`), LaTeX math rendering (`@streamdown/math`), and CJK text support (`@streamdown/cjk`). It also includes `remend`, a standalone utility for self-healing markdown that powers Streamdown's incomplete block completion logic. Built with performance in mind, Streamdown features memoized rendering, lazy-loaded language support, and token caching for efficient updates during high-frequency streaming scenarios.
## Streamdown Component
The main `Streamdown` component renders Markdown content with streaming support, handling incomplete syntax automatically. It accepts children as a string and supports both streaming and static modes.
```tsx
import { Streamdown } from 'streamdown';
import { code } from '@streamdown/code';
import { mermaid } from '@streamdown/mermaid';
import { math } from '@streamdown/math';
import { cjk } from '@streamdown/cjk';
import 'katex/dist/katex.min.css';
import 'streamdown/styles.css';
// Basic usage - streaming mode (default)
function BasicExample() {
const markdown = "# Hello World\n\nThis is **streaming** markdown!";
return {markdown};
}
// Full-featured example with AI SDK
import { useChat } from '@ai-sdk/react';
function ChatExample() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat();
return (
{messages.map((message) => (
{message.content}
))}
);
}
// Static mode for pre-generated content (blogs, docs)
function StaticExample({ content }: { content: string }) {
return (
{content}
);
}
```
## StreamdownProps Interface
The complete props interface for configuring the Streamdown component behavior, styling, and plugins.
```tsx
import type { StreamdownProps, ControlsConfig, LinkSafetyConfig } from 'streamdown';
// Full StreamdownProps configuration
const props: StreamdownProps = {
// Core props
children: "# Markdown content",
mode: "streaming", // "streaming" | "static"
parseIncompleteMarkdown: true,
isAnimating: false,
className: "my-markdown",
// Styling
shikiTheme: ['github-light', 'github-dark'], // [light, dark] themes
caret: "block", // "block" | "circle" - shows cursor at end
animated: true, // Enable fade-in animation
// Plugins
plugins: {
code: code, // Syntax highlighting
mermaid: mermaid, // Diagram rendering
math: math, // LaTeX math
cjk: cjk, // CJK text support
},
// Interactive controls
controls: {
table: true, // Table copy/download buttons
code: true, // Code copy/download buttons
mermaid: {
download: true, // Download SVG
copy: true, // Copy source
fullscreen: true, // Fullscreen mode
panZoom: true, // Pan and zoom
},
},
// Link safety (warns before opening external links)
linkSafety: {
enabled: true,
onLinkCheck: async (url) => {
// Return true if link is safe, false to show warning
return url.startsWith('https://trusted-domain.com');
},
renderModal: ({ url, isOpen, onClose, onConfirm }) => (
Open external link: {url}?
),
},
// Custom HTML tags (for AI tool outputs, citations, etc.)
allowedTags: {
source: ["id"],
mention: ["user_id", "type"],
},
// Remend options (incomplete markdown completion)
remend: {
bold: true,
italic: true,
links: true,
images: true,
inlineCode: true,
strikethrough: true,
katex: true,
linkMode: 'protocol', // 'protocol' | 'text-only'
},
};
```
## Code Highlighting Plugin (@streamdown/code)
Provides syntax highlighting for code blocks using Shiki, with support for 200+ languages that are lazy-loaded on demand.
```tsx
import { code, createCodePlugin } from '@streamdown/code';
import { Streamdown } from 'streamdown';
// Use default configuration
{`
\`\`\`typescript
const greeting: string = "Hello, World!";
console.log(greeting);
\`\`\`
`}
// Custom themes configuration
const customCode = createCodePlugin({
themes: ['one-light', 'one-dark-pro'], // [light, dark]
});
{markdown}
// Plugin methods
code.supportsLanguage('typescript'); // true
code.supportsLanguage('python'); // true
code.getSupportedLanguages(); // ['javascript', 'typescript', 'python', ...]
code.getThemes(); // ['github-light', 'github-dark']
// Highlight code programmatically
code.highlight(
{ code: 'const x = 1;', language: 'typescript', themes: ['github-light', 'github-dark'] },
(result) => console.log('Tokens:', result.tokens)
);
```
## Math Plugin (@streamdown/math)
Renders LaTeX math expressions using KaTeX, supporting both inline and block math notation.
```tsx
import { math, createMathPlugin } from '@streamdown/math';
import { Streamdown } from 'streamdown';
import 'katex/dist/katex.min.css'; // Required CSS import
// Default usage (double $$ only)
{`
Inline math: $$E = mc^2$$
Block math:
$$
\\int_{-\\infty}^{\\infty} e^{-x^2} dx = \\sqrt{\\pi}
$$
Matrix:
$$
\\begin{bmatrix}
a & b \\\\
c & d
\\end{bmatrix}
$$
`}
// Enable single dollar sign syntax ($...$)
const mathWithSingleDollar = createMathPlugin({
singleDollarTextMath: true, // Enables $inline$ syntax (default: false)
errorColor: '#ff0000', // Color for KaTeX errors
});
{`Price is $50 (not math) vs $E=mc^2$ (is math with singleDollarTextMath enabled)`}
// Get CSS path for reference
math.getStyles(); // "katex/dist/katex.min.css"
```
## Mermaid Plugin (@streamdown/mermaid)
Renders Mermaid diagrams from code blocks with interactive controls for fullscreen, download, and pan/zoom.
```tsx
import { mermaid, createMermaidPlugin } from '@streamdown/mermaid';
import { Streamdown } from 'streamdown';
// Default usage
{`
\`\`\`mermaid
graph TD
A[Start] --> B{Decision}
B -->|Yes| C[Do something]
B -->|No| D[Do something else]
C --> E[End]
D --> E
\`\`\`
`}
// Custom Mermaid configuration
const customMermaid = createMermaidPlugin({
config: {
theme: 'dark', // 'default' | 'dark' | 'forest' | 'neutral' | 'base'
fontFamily: 'monospace',
securityLevel: 'strict',
},
});
(
Failed to render diagram: {error}
{chart}
),
}}
controls={{
mermaid: {
download: true, // SVG download button
copy: true, // Copy source button
fullscreen: true, // Fullscreen toggle
panZoom: true, // Pan and zoom controls
},
}}
>
{markdown}
// Supported diagram types: flowchart, sequence, state, class, pie, gantt, ER, git
```
## CJK Plugin (@streamdown/cjk)
Improves rendering of Chinese, Japanese, and Korean text by fixing emphasis markers and autolink handling near CJK punctuation.
```tsx
import { cjk, createCjkPlugin } from '@streamdown/cjk';
import { Streamdown } from 'streamdown';
// Enable CJK support
{`
日本語の**太字**テスト
中文的*斜体*测试
한국어~~취소선~~테스트
Link: https://example.com。この後にテキスト
`}
// Create plugin instance
const cjkPlugin = createCjkPlugin();
// Plugin provides remark plugins that run before and after remarkGfm
console.log(cjkPlugin.remarkPluginsBefore); // [remarkCjkFriendly]
console.log(cjkPlugin.remarkPluginsAfter); // [remarkCjkAutolinkBoundary, remarkCjkFriendlyGfmStrikethrough]
// Supported CJK punctuation for autolink boundaries:
// 。.,、?!:;()【】「」『』〈〉《》
```
## Remend (Incomplete Markdown Completion)
Standalone utility that auto-completes incomplete Markdown syntax during streaming. Powers Streamdown's streaming mode.
```typescript
import remend, { type RemendHandler, type RemendOptions } from 'remend';
import {
isWithinCodeBlock,
isWithinMathBlock,
isWithinLinkOrImageUrl,
isWordChar,
} from 'remend';
// Basic usage - completes incomplete markdown
remend("This is **bold text"); // "This is **bold text**"
remend("Check out [link](https://"); // "Check out [link](streamdown:incomplete-link)"
remend("Here's `code"); // "Here's `code`"
remend("Strike ~~this"); // "Strike ~~this~~"
remend("Math $$E = mc"); // "Math $$E = mc$$"
// Configure specific completions
const options: RemendOptions = {
bold: true,
italic: true,
boldItalic: true,
links: true,
images: true, // Incomplete images are removed entirely
inlineCode: true,
strikethrough: true,
katex: true,
setextHeadings: true,
linkMode: 'text-only', // 'protocol' | 'text-only'
};
remend("Check [this](", options); // "Check this" (text-only mode)
remend("Check [this](", { linkMode: 'protocol' }); // "Check [this](streamdown:incomplete-link)"
// Custom handlers
const jokeHandler: RemendHandler = {
name: 'joke',
handle: (text) => {
if (text.includes('<<>>') && !text.includes('<<>>')) {
return text + '<<>>';
}
return text;
},
priority: 80, // Built-in priorities: 0-70, default: 100
};
remend("<<>>Why did the", { handlers: [jokeHandler] });
// "<<>>Why did the<<>>"
// Utility functions for context detection in custom handlers
const text = "```js\nconst x = **bold";
isWithinCodeBlock(text, text.length - 1); // true
isWithinMathBlock("$$E = mc", 5); // true
isWithinLinkOrImageUrl("[text](http://", 10); // true
isWordChar('a'); // true
isWordChar(' '); // false
```
## Custom Components
Override default HTML element rendering with custom React components for full styling control.
```tsx
import { Streamdown, type Components, type ExtraProps } from 'streamdown';
// Define custom components
const components: Components = {
// Headings
h1: ({ children, node, ...props }) => (
,
ol: ({ children }) => {children},
li: ({ children }) =>
{children}
,
// Tables
table: ({ children }) => (
{children}
),
th: ({ children }) =>
{children}
,
td: ({ children }) =>
{children}
,
// Blockquotes
blockquote: ({ children }) => (
{children}
),
// Images
img: ({ src, alt, ...props }) => (
),
};
{markdown}
```
## Allowed Tags (Custom HTML Elements)
Add custom HTML tags to support AI tool outputs, citations, mentions, and other domain-specific elements.
```tsx
import { Streamdown, type AllowedTags } from 'streamdown';
// Define custom tags with their allowed attributes
const allowedTags: AllowedTags = {
source: ["id", "type"],
mention: ["user_id", "name"],
widget: ["data-*"], // Wildcard: all data-* attributes
citation: ["ref", "page"],
};
// Custom components to render the tags
const components = {
source: ({ id, type, children }: { id: string; type?: string; children?: React.ReactNode }) => (
[{id}] {children}
),
mention: ({ user_id, name }: { user_id: string; name?: string }) => (
@{name || user_id}
),
citation: ({ ref, page }: { ref: string; page?: string }) => (
[{ref}{page ? `:${page}` : ''}]
),
};
// Usage with AI-generated content containing custom tags
{`
According to the research Smith et al. 2024, the findings show...
Hey John, check this out!
The theory was first proposed in 1905 .
`}
// Note: allowedTags only works with default rehype plugins
```
## Default Plugins and URL Transform
Access and customize the default remark/rehype plugins and URL transformation behavior.
```tsx
import {
Streamdown,
defaultRemarkPlugins,
defaultRehypePlugins,
defaultUrlTransform,
type UrlTransform,
} from 'streamdown';
import type { Pluggable } from 'unified';
import remarkEmoji from 'remark-emoji';
import rehypeSlug from 'rehype-slug';
// View default plugins
console.log(defaultRemarkPlugins);
// { gfm: [remarkGfm, {}] }
console.log(defaultRehypePlugins);
// { raw: rehypeRaw, sanitize: [rehypeSanitize, schema], harden: [harden, config] }
// Add custom plugins alongside defaults
const customRemarkPlugins: Pluggable[] = [
...Object.values(defaultRemarkPlugins),
remarkEmoji,
];
const customRehypePlugins: Pluggable[] = [
...Object.values(defaultRehypePlugins),
rehypeSlug,
];
{markdown}
// Custom URL transform (for rewriting URLs)
const customUrlTransform: UrlTransform = (url, key, node) => {
// Proxy images through your CDN
if (key === 'src' && url.startsWith('http')) {
return `https://cdn.example.com/proxy?url=${encodeURIComponent(url)}`;
}
// Add tracking to links
if (key === 'href' && url.startsWith('http')) {
return `${url}?ref=myapp`;
}
// Use default for everything else
return defaultUrlTransform(url, key, node);
};
{markdown}
// Disable HTML entirely
const { raw, ...restRehype } = defaultRehypePlugins;
{markdown}
```
## Animation and Carets
Configure streaming animations and cursor carets for visual feedback during AI response generation.
```tsx
import { Streamdown, createAnimatePlugin, type AnimateOptions } from 'streamdown';
import 'streamdown/styles.css'; // Required for animations
// Basic animated streaming with caret
{streamingContent}
// Custom animation configuration
const animateOptions: AnimateOptions = {
duration: 300, // Animation duration in ms
stagger: 50, // Delay between elements
};
{content}
// Per-message caret in chat (only show on latest streaming message)
{messages.map((msg, index) => (
{msg.content}
))}
// Create custom animate plugin for advanced use
const customAnimate = createAnimatePlugin({
duration: 200,
stagger: 30,
});
// Plugin provides: customAnimate.rehypePlugin
```
## parseMarkdownIntoBlocks
Utility function to split Markdown into logical blocks for streaming optimization. Used internally by Streamdown.
```typescript
import { parseMarkdownIntoBlocks } from 'streamdown';
const markdown = `
# Heading
First paragraph with some text.
- List item 1
- List item 2
\`\`\`javascript
const code = "block";
\`\`\`
Second paragraph.
`;
const blocks = parseMarkdownIntoBlocks(markdown);
console.log(blocks);
// [
// "# Heading",
// "First paragraph with some text.",
// "- List item 1\n- List item 2",
// "```javascript\nconst code = \"block\";\n```",
// "Second paragraph."
// ]
// Custom block parsing function
const customParser = (md: string): string[] => {
// Your custom logic
return md.split('\n\n').filter(Boolean);
};
{markdown}
// Custom Block component for advanced rendering
const CustomBlock = ({ content, index, ...props }) => (
{content}
);
{markdown}
```
Streamdown is primarily designed for AI chat interfaces where real-time Markdown rendering is essential. It integrates seamlessly with the Vercel AI SDK's `useChat` hook, handling streaming responses with proper formatting even when tokens arrive in incomplete states. The library is also well-suited for documentation sites, blog platforms, and any application where Markdown content needs syntax highlighting, math equations, or diagram rendering.
Integration patterns typically involve wrapping AI response content in the Streamdown component with appropriate plugins enabled. For production applications, it's recommended to install only the plugins you need to minimize bundle size. The `isAnimating` prop should be connected to your streaming state to enable carets and disable interactive controls during generation. For static content like blog posts or documentation, use `mode="static"` to skip streaming optimizations and render content as a single unit.