Documentation Index
Fetch the complete documentation index at: https://mintlify.com/vercel/streamdown/llms.txt
Use this file to discover all available pages before exploring further.
The renderers field on PluginConfig lets you map code fence languages to custom React components. Use this to render Vega-Lite charts, D2 diagrams, infographics, or any visualization — without modifying Streamdown’s internals.
Custom renderers take priority over the default code block. If a renderer matches the language, it renders instead of CodeBlock. You can even override the mermaid language by registering a renderer for it.
Basic usage
Pass an array of { language, component } objects to plugins.renderers:
import { Streamdown } from "streamdown";
import type { CustomRendererProps } from "streamdown";
const MyRenderer = ({ code, language, isIncomplete }: CustomRendererProps) => {
if (isIncomplete) {
return <div className="loading">Loading...</div>;
}
return <MyVisualization data={code} />;
};
export default function Chat() {
return (
<Streamdown
plugins={{
renderers: [
{ language: "my-lang", component: MyRenderer },
],
}}
>
{markdown}
</Streamdown>
);
}
CustomRenderer interface
interface CustomRenderer {
language: string | string[];
component: React.ComponentType<CustomRendererProps>;
}
language accepts a single string or an array of strings. All listed language identifiers map to the same component.
CustomRendererProps
interface CustomRendererProps {
code: string;
isIncomplete: boolean;
language: string;
meta?: string;
}
The raw text content inside the code fence, exactly as it appears in the markdown source.
true while the code fence is still being streamed (closing triple-backticks have not arrived yet). Use this to show a loading state and avoid attempting to parse incomplete data.
The language identifier from the opening fence, e.g. "vega-lite", "d2", "infographic".
Everything after the language identifier on the opening fence line. For example, in ```rust {1} title="foo", meta is '{1} title="foo"'. Undefined when no metastring is present.
Multiple languages per renderer
const renderers = [
{ language: ["vega", "vega-lite"], component: VegaLiteRenderer },
{ language: "d2", component: D2Renderer },
];
<Streamdown plugins={{ renderers }}>
{markdown}
</Streamdown>
Streaming considerations
During streaming, isIncomplete is true while the code fence is being written. For most renderers, show a placeholder and wait for the complete input before attempting to parse or render:
const MyRenderer = ({ code, isIncomplete }: CustomRendererProps) => {
if (isIncomplete) {
return (
<div className="loading-placeholder">
<span>Rendering...</span>
</div>
);
}
return <MyVisualization data={code} />;
};
Some libraries support progressive rendering (e.g., AntV Infographic). In those cases you can render on every update and skip the isIncomplete guard.
Reusing built-in components
Streamdown exports its internal code block components so your custom renderers can match the default styling:
import {
CodeBlockContainer,
CodeBlockHeader,
CodeBlockCopyButton,
} from "streamdown";
import type { CustomRendererProps } from "streamdown";
export const VegaLiteRenderer = ({
code,
language,
isIncomplete,
}: CustomRendererProps) => {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isIncomplete || !containerRef.current) return;
let cancelled = false;
const render = async () => {
const spec = JSON.parse(code);
const vegaEmbed = (await import("vega-embed")).default;
if (cancelled || !containerRef.current) return;
containerRef.current.innerHTML = "";
await vegaEmbed(containerRef.current, spec, {
actions: false,
renderer: "svg",
});
};
render();
return () => { cancelled = true; };
}, [code, isIncomplete]);
return (
<CodeBlockContainer isIncomplete={isIncomplete} language={language}>
<CodeBlockHeader language={language} />
{isIncomplete ? (
<div className="flex h-48 items-center justify-center">
<span>Loading chart...</span>
</div>
) : (
<div ref={containerRef} className="overflow-hidden rounded-md p-4" />
)}
</CodeBlockContainer>
);
};
Available exported components
| Component | Description |
|---|
CodeBlock | Full code block with syntax highlighting and controls |
CodeBlockContainer | Outer wrapper with border and rounded corners |
CodeBlockHeader | Language label header bar |
CodeBlockCopyButton | Standalone copy-to-clipboard button |
CodeBlockDownloadButton | Standalone download button |
CodeBlockSkeleton | Loading skeleton placeholder |
How renderers integrate with the plugin pipeline
When Streamdown encounters a code fence, it checks renderers before falling back to the default code block:
- Custom renderers — first match by language wins
- Mermaid plugin — if
plugins.mermaid is configured and the language is "mermaid"
- Default code block — with optional syntax highlighting from
plugins.code
import { code } from "@streamdown/code";
import { mermaid } from "@streamdown/mermaid";
<Streamdown
plugins={{
code,
mermaid,
renderers: [
{ language: "vega-lite", component: VegaLiteRenderer },
{ language: "d2", component: D2Renderer },
],
}}
>
{markdown}
</Streamdown>
Type reference
interface CustomRendererProps {
code: string;
isIncomplete: boolean;
language: string;
meta?: string;
}
interface CustomRenderer {
language: string | string[];
component: React.ComponentType<CustomRendererProps>;
}
interface PluginConfig {
cjk?: CjkPlugin;
code?: CodeHighlighterPlugin;
math?: MathPlugin;
mermaid?: DiagramPlugin;
renderers?: CustomRenderer[];
}