Skip to main content
Streamdown renders Markdown elements using a built-in set of React components. The components prop lets you replace any of these with your own implementation, giving you full control over the rendered HTML structure, styling, and behavior.

Basic usage

app/page.tsx
<Streamdown
  components={{
    h1: ({ children }) => (
      <h1 className="text-4xl font-bold text-blue-600">{children}</h1>
    ),
    p: ({ children }) => (
      <p className="text-gray-700 leading-relaxed">{children}</p>
    ),
  }}
>
  {markdown}
</Streamdown>

Overridable elements

You can supply a custom component for any of the following keys:
KeyElementDefault styles
h1<h1>mt-6 mb-2 font-semibold text-3xl
h2<h2>mt-6 mb-2 font-semibold text-2xl
h3<h3>mt-6 mb-2 font-semibold text-xl
h4<h4>mt-6 mb-2 font-semibold text-lg
h5<h5>mt-6 mb-2 font-semibold text-base
h6<h6>mt-6 mb-2 font-semibold text-sm
p<p>
strong<span>font-semibold
a<a> or <button>font-medium text-primary underline
ul<ul>list-inside list-disc whitespace-normal [li_&]:pl-6
ol<ol>list-inside list-decimal whitespace-normal [li_&]:pl-6
li<li>py-1 [&>p]:inline
blockquote<blockquote>my-4 border-muted-foreground/30 border-l-4 pl-4 text-muted-foreground italic
hr<hr>my-6 border-border
tablewrapped <table>Table with controls panel
thead<thead>bg-muted/80
tbody<tbody>divide-y divide-border
tr<tr>border-border
th<th>whitespace-nowrap px-4 py-2 text-left font-semibold text-sm
td<td>px-4 py-2 text-sm
codeCode block + inline codeSyntax-highlighted <CodeBlock> or <code>
img<div> with image + fallbackResponsive image container
sup<sup>text-sm
sub<sub>text-sm
section<section>Footnotes handling
inlineCode<code> (inline only)Virtual key — see below

Component props (TypeScript types)

Each component in the components map receives a union of:
  • The standard HTML element props for its tag (e.g. React.ComponentProps<"h1">)
  • The ExtraProps type, which adds:
    • node — the HAST (Hypertext Abstract Syntax Tree) node for this element, useful for reading raw AST properties
You can import these types from Streamdown:
import type { Components, ExtraProps } from 'streamdown';

const myComponents: Partial<Components> = {
  h2: ({ children, node }) => {
    // node.position has source line/column info
    return <h2 data-line={node?.position?.start?.line}>{children}</h2>;
  },
};

Important: default styles are not inherited

A custom component fully replaces the default implementation, including all of its built-in Tailwind classes. The className prop passed to your component contains only classes from the Markdown AST (e.g. language-js on <code>) — not the default visual styles.If you want to preserve the default appearance and add to it, you must include the original classes manually.
app/page.tsx
<Streamdown
  components={{
    // ❌ loses default mt-6 mb-2 font-semibold text-2xl spacing
    h2: ({ children }) => (
      <h2 className="text-blue-500">{children}</h2>
    ),

    // ✅ preserves default styles and adds color
    h2: ({ children, className }) => (
      <h2 className={`mt-6 mb-2 font-semibold text-2xl text-blue-500 ${className ?? ''}`}>
        {children}
      </h2>
    ),
  }}
>
  {markdown}
</Streamdown>
If you only need visual changes (colors, fonts, spacing) without changing the element structure, use data-streamdown CSS selectors instead. See the Styling page.

Inline code: the inlineCode key

The code key covers both fenced code blocks (with syntax highlighting, copy buttons, and Mermaid support) and inline code spans. Overriding code replaces the entire pipeline. To customize only inline code spans without affecting block code, use the special inlineCode virtual key:
app/page.tsx
<Streamdown
  components={{
    inlineCode: ({ children }) => (
      <code className="rounded bg-violet-100 px-1.5 py-0.5 text-violet-800 text-sm">
        {children}
      </code>
    ),
  }}
>
  {markdown}
</Streamdown>
You can combine both: inlineCode handles inline spans while code handles fenced code blocks.
app/page.tsx
<Streamdown
  components={{
    inlineCode: ({ children }) => (
      <code className="bg-violet-100 text-violet-800 rounded px-1 text-sm">
        {children}
      </code>
    ),
    code: MyCustomCodeBlock, // only receives block code
  }}
>
  {markdown}
</Streamdown>

Streaming state in custom components

The useIsCodeFenceIncomplete hook lets a custom code component detect whether its code fence is still being streamed. This is useful for deferring expensive rendering (syntax highlighting, diagram parsing) until the block is complete.
app/page.tsx
import { Streamdown, useIsCodeFenceIncomplete } from 'streamdown';

function MyCodeBlock({ children }: { children: React.ReactNode }) {
  const isIncomplete = useIsCodeFenceIncomplete();

  if (isIncomplete) {
    return <div className="animate-pulse bg-muted h-24 rounded" />;
  }

  return <pre><code>{children}</code></pre>;
}

<Streamdown components={{ code: MyCodeBlock }} isAnimating={isStreaming}>
  {markdown}
</Streamdown>
The hook returns true when all three conditions are met:
  1. isAnimating={true} on the parent <Streamdown>
  2. The component is in the last block being rendered
  3. That block contains an unclosed code fence (an opening ``` without a closing ```)
Once the fence closes, the hook returns false and your component renders normally — even while the rest of the markdown continues streaming.
app/page.tsx
<Streamdown
  components={{
    a: ({ href, children, ...props }) => (
      <a
        href={href}
        className="text-purple-600 hover:text-purple-800 underline"
        target="_blank"
        rel="noreferrer"
        {...props}
      >
        {children}
      </a>
    ),
  }}
>
  {markdown}
</Streamdown>
Overriding the a component bypasses the built-in link-safety modal. If you need link safety, implement your own confirmation flow inside the custom component or leave a unoverridden.

Preserving table interactivity

When you override the table component, the built-in copy and download action buttons are lost. To restore them, import the action components and place them inside your custom table wrapper:
app/page.tsx
import {
  Streamdown,
  TableCopyDropdown,
  TableDownloadDropdown,
} from 'streamdown';

<Streamdown
  components={{
    table: ({ children, className }) => (
      <div data-streamdown="table-wrapper">
        <div className="flex items-center justify-end gap-1">
          <TableCopyDropdown />
          <TableDownloadDropdown />
        </div>
        <MyCustomTable className={className}>{children}</MyCustomTable>
      </div>
    ),
  }}
>
  {markdown}
</Streamdown>
The data-streamdown="table-wrapper" attribute is required. The action components use .closest('[data-streamdown="table-wrapper"]') to find their parent container, then .querySelector('table') to locate the data. Your custom table component must render a <table> element as a descendant.
For single-format download buttons, use TableDownloadButton:
import { TableDownloadButton } from 'streamdown';

<TableDownloadButton format="csv" />
For fully custom implementations, use the low-level extraction utilities:
import {
  extractTableDataFromElement,
  tableDataToCSV,
  tableDataToTSV,
  tableDataToMarkdown,
} from 'streamdown';

const data = extractTableDataFromElement(tableElement);
const csv = tableDataToCSV(data);

Custom HTML tags

You can render custom tags from AI responses (like <mention>, <source>, etc.) by combining allowedTags with components:
app/page.tsx
<Streamdown
  allowedTags={{
    mention: ['user_id'],
    source: ['id'],
  }}
  components={{
    mention: ({ user_id, children }) => (
      <span className="text-blue-600">@{children}</span>
    ),
    source: ({ id, children }) => (
      <button
        className="text-blue-600 underline cursor-pointer"
        onClick={() => navigate(`/sources/${id}`)}
      >
        {children}
      </button>
    ),
  }}
>
  {markdown}
</Streamdown>
If tag content contains underscores or asterisks that should not be formatted as Markdown, add the tag to literalTagContent:
<Streamdown
  allowedTags={{ mention: ['user_id'] }}
  literalTagContent={['mention']}
  components={{
    mention: ({ children }) => <span>@{children}</span>,
  }}
>
  {markdown}
</Streamdown>
See the Configuration page for the full allowedTags and literalTagContent prop references.

Build docs developers (and LLMs) love