Skip to main content
Streamdown is a drop-in replacement for react-markdown. You can use it anywhere you render markdown — in an AI chat interface, a blog post, or a documentation page.

Basic usage with the AI SDK

Streamdown is designed to work with AI streaming out of the box. The key props are:
  • isAnimating — set to true while the stream is active so Streamdown knows content is incomplete
  • animated — enables character-by-character animation as tokens arrive
  • plugins — opt-in plugin bundles for code highlighting, math, diagrams, and CJK
app/chat.tsx
"use client";

import { useChat } from "@ai-sdk/react";
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";

export default function Chat() {
  const { messages, status } = useChat();

  return (
    <div>
      {messages.map((message) => (
        <div key={message.id}>
          {message.role === "user" ? "User: " : "AI: "}
          {message.parts.map((part, index) =>
            part.type === "text" ? (
              <Streamdown
                key={index}
                animated
                plugins={{ code, mermaid, math, cjk }}
                isAnimating={status === "streaming"}
              >
                {part.text}
              </Streamdown>
            ) : null,
          )}
        </div>
      ))}
    </div>
  );
}
Each plugin is optional. Install only the ones you need and add matching @source entries to your globals.css. See Getting started for Tailwind setup details.

Streaming mode vs static mode

Streamdown operates in two modes, controlled by the mode prop.
Streaming mode (mode="streaming") is the default. It is optimized for content that arrives incrementally:
  • Parses markdown into independent blocks so already-rendered content is not re-rendered when new tokens arrive
  • Applies the remend preprocessor to gracefully handle unterminated markdown constructs (unclosed code fences, incomplete bold, etc.)
  • Uses useTransition to keep the UI responsive during rapid updates
  • Tracks isAnimating state to disable copy buttons while content is live
<Streamdown isAnimating={status === "streaming"}>
  {streamingContent}
</Streamdown>

Props overview

isAnimating

Set isAnimating={true} while the stream is active. This disables interactive controls (copy buttons, download buttons) until streaming completes, preventing user actions on partial content. It also gates the caret display and the animation plugin.
<Streamdown isAnimating={status === "streaming"}>
  {content}
</Streamdown>

animated

Enables character-by-character animation as new tokens arrive. Pass true for default animation settings, or an AnimateOptions object to customize behavior.
// Default animation
<Streamdown animated isAnimating={isStreaming}>
  {content}
</Streamdown>

// Custom animation options
<Streamdown animated={{ animation: "fadeIn" }} isAnimating={isStreaming}>
  {content}
</Streamdown>
The animated prop requires streamdown/styles.css to be imported in your app.

mode

Controls the rendering strategy. Defaults to "streaming".
// Streaming (default) — optimized for incremental updates
<Streamdown mode="streaming">{liveContent}</Streamdown>

// Static — optimized for complete, pre-generated content
<Streamdown mode="static">{finishedContent}</Streamdown>

dir

Sets the text direction for rendered blocks. Use "auto" to detect direction per-block using the first strong character algorithm — useful for mixed-language AI responses.
// Explicit right-to-left
<Streamdown dir="rtl">{arabicContent}</Streamdown>

// Auto-detect per block
<Streamdown dir="auto">{mixedLanguageContent}</Streamdown>

caret

Shows a visual caret indicator at the end of the streaming output. Only visible while isAnimating is true. Hidden automatically when the last block is an incomplete code fence or table.
// Block cursor: ▋
<Streamdown caret="block" isAnimating={isStreaming}>
  {content}
</Streamdown>

// Circle cursor: ●
<Streamdown caret="circle" isAnimating={isStreaming}>
  {content}
</Streamdown>

plugins

Opt-in plugin bundles for extended functionality. Each plugin is a separate package.
import { code } from "@streamdown/code";
import { mermaid } from "@streamdown/mermaid";
import { math } from "@streamdown/math";
import { cjk } from "@streamdown/cjk";

<Streamdown plugins={{ code, mermaid, math, cjk }}>
  {content}
</Streamdown>
Plugin keyPackageProvides
code@streamdown/codeShiki syntax highlighting for code blocks
mermaid@streamdown/mermaidMermaid diagram rendering
math@streamdown/mathLaTeX math via KaTeX
cjk@streamdown/cjkCJK-friendly word breaking

Custom component overrides

Pass a components map to replace how any HTML element is rendered. This follows the same API as react-markdown.
import { Streamdown } from "streamdown";
import type { Components } from "streamdown";

const components: Components = {
  // Custom link renderer
  a: ({ href, children }) => (
    <a href={href} target="_blank" rel="noopener noreferrer">
      {children}
    </a>
  ),
  // Custom blockquote
  blockquote: ({ children }) => (
    <blockquote className="border-l-4 border-blue-500 pl-4 italic">
      {children}
    </blockquote>
  ),
  // Custom inline code (separate from block code)
  inlineCode: ({ children }) => (
    <code className="bg-gray-100 px-1 rounded text-sm font-mono">
      {children}
    </code>
  ),
};

export default function Page() {
  return (
    <Streamdown components={components}>
      {markdown}
    </Streamdown>
  );
}
The inlineCode key is a Streamdown-specific shorthand. When provided, it overrides inline code elements only, while block code elements continue using Streamdown’s built-in code block renderer.

Custom rehype and remark plugins

Streamdown includes a default set of plugins: rehype-raw, rehype-sanitize, rehype-harden, and remark-gfm. You can extend or replace these by passing rehypePlugins or remarkPlugins.
import { Streamdown, defaultRehypePlugins, defaultRemarkPlugins } from "streamdown";
import rehypeSlug from "rehype-slug";
import remarkDirective from "remark-directive";

// Extend defaults with additional plugins
const rehypePlugins = [
  ...Object.values(defaultRehypePlugins),
  rehypeSlug,
];

const remarkPlugins = [
  ...Object.values(defaultRemarkPlugins),
  remarkDirective,
];

export default function Page() {
  return (
    <Streamdown
      rehypePlugins={rehypePlugins}
      remarkPlugins={remarkPlugins}
    >
      {markdown}
    </Streamdown>
  );
}
Replacing rehypePlugins entirely removes the default security plugins (rehype-sanitize, rehype-harden). If you replace the default plugins, ensure you include appropriate sanitization for your use case.
Exported defaults for extension:
import { defaultRehypePlugins, defaultRemarkPlugins } from "streamdown";

// defaultRehypePlugins contains: raw, sanitize, harden
// defaultRemarkPlugins contains: gfm, codeMeta

Block-level rendering

In streaming mode, Streamdown splits markdown into independent blocks before rendering. Each block is a memoized Block component — only the last (incomplete) block re-renders as new tokens arrive. Previously completed blocks are not touched. You can provide a custom BlockComponent to wrap each block, or a custom parseMarkdownIntoBlocksFn if you need different block splitting logic:
import { Streamdown, type BlockProps } from "streamdown";

function AnnotatedBlock({ content, index, isIncomplete, ...props }: BlockProps) {
  return (
    <div data-block-index={index} data-incomplete={isIncomplete}>
      {/* Streamdown renders Block internals via ...props */}
    </div>
  );
}

export default function Page() {
  return (
    <Streamdown BlockComponent={AnnotatedBlock}>
      {markdown}
    </Streamdown>
  );
}
The BlockProps type includes:
  • content — the raw markdown string for this block
  • index — zero-based position in the block list
  • isIncompletetrue when this is the last block and isAnimating is true with an unclosed code fence
  • dir — resolved text direction for the block (when dir="auto" is set on Streamdown)

Build docs developers (and LLMs) love