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
<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:
| Key | Element | Default 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 |
table | wrapped <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 |
code | Code block + inline code | Syntax-highlighted <CodeBlock> or <code> |
img | <div> with image + fallback | Responsive 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.
<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:
<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.
<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.
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:
isAnimating={true} on the parent <Streamdown>
- The component is in the last block being rendered
- 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.
Custom links
<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:
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);
You can render custom tags from AI responses (like <mention>, <source>, etc.) by combining allowedTags with components:
<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.