# 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 }) => (

{children}

), h2: ({ children, ...props }) => (

{children}

), // Text formatting p: ({ children, ...props }) => (

{children}

), strong: ({ children }) => {children}, em: ({ children }) => {children}, // Links with custom behavior a: ({ href, children, ...props }) => ( {children} ), // Code blocks code: ({ children, className, ...props }) => { const isInline = !className?.includes('language-'); return isInline ? ( {children} ) : ( {children} ); }, pre: ({ children, ...props }) => (
      {children}
    
), // Lists ul: ({ children }) => , 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 }) => ( {alt} ), }; {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.