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[];
}