Register custom renderers for arbitrary code fence languages.
The renderers field on PluginConfig lets you register custom React components for arbitrary code fence languages. Use this to render Vega-Lite charts, AntV infographics, D2 diagrams, PlantUML, or any other visualization — without forking Streamdown.
Custom renderers take priority over default code blocks. If a custom renderer matches a language, it renders instead of the default CodeBlock. You can even override mermaid by registering a renderer for the "mermaid" language.
Pass an array of { language, component } objects to plugins.renderers:
import { Streamdown } from "streamdown";
import type { CustomRendererProps } from "streamdown";
import { VegaLiteRenderer } from "./vega-lite-renderer";
export default function Chat() {
return (
<Streamdown
plugins={{
renderers: [
{ language: "vega-lite", component: VegaLiteRenderer },
],
}}
>
{markdown}
</Streamdown>
);
}The language field accepts a string or an array of strings:
const renderers = [
{ language: ["vega", "vega-lite"], component: VegaLiteRenderer },
{ language: "infographic", component: InfographicRenderer },
{ language: "d2", component: D2Renderer },
];
<Streamdown plugins={{ renderers }}>{markdown}</Streamdown>Every custom renderer receives these props:
| Prop | Type | Description |
|---|---|---|
code | string | The raw text content inside the code fence |
language | string | The language identifier from the code fence |
isIncomplete | boolean | true while the code fence is still being streamed |
Streamdown exports its internal code block components so your custom renderers can reuse them for consistent styling:
import { useEffect, useRef } from "react";
import type { CustomRendererProps } from "streamdown";
import { CodeBlockContainer, CodeBlockHeader } 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 rounded-md bg-muted">
<span className="text-muted-foreground text-sm">
Loading chart...
</span>
</div>
) : (
<div ref={containerRef} className="overflow-hidden rounded-md p-4" />
)}
</CodeBlockContainer>
);
};Exported components
| Component | Description |
|---|---|
CodeBlock | Full code block with syntax highlighting and controls |
CodeBlockContainer | Outer wrapper with border and styling |
CodeBlockHeader | Language label header |
CodeBlockCopyButton | Copy-to-clipboard button |
CodeBlockDownloadButton | Download button |
CodeBlockSkeleton | Loading skeleton placeholder |
Vega-Lite charts
Vega-Lite is a grammar for interactive graphics. Install vega, vega-lite, and vega-embed, then create a renderer that parses the JSON spec and calls vegaEmbed:
npm install vega vega-lite vega-embedThe renderer from the reusing built-in components section above is a complete Vega-Lite implementation. Once registered, your AI can output charts like:
```vega-lite
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"width": "container",
"height": 200,
"data": {
"values": [
{"month": "Jan", "revenue": 28},
{"month": "Feb", "revenue": 55},
{"month": "Mar", "revenue": 43}
]
},
"mark": "bar",
"encoding": {
"x": {"field": "month", "type": "nominal"},
"y": {"field": "revenue", "type": "quantitative"}
}
}
```AntV Infographic
AntV Infographic renders rich infographic diagrams from a YAML-like DSL that supports streaming output. Install the package, then create a renderer:
npm install @antv/infographicimport { useEffect, useRef } from "react";
import type { CustomRendererProps } from "streamdown";
import { CodeBlockContainer, CodeBlockHeader } from "streamdown";
export const InfographicRenderer = ({
code,
language,
isIncomplete,
}: CustomRendererProps) => {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) {
return;
}
let cancelled = false;
const render = async () => {
const { Infographic } = await import("@antv/infographic");
if (cancelled || !containerRef.current) {
return;
}
containerRef.current.innerHTML = "";
new Infographic({
container: containerRef.current,
text: code,
});
};
render();
return () => {
cancelled = true;
};
}, [code]);
return (
<CodeBlockContainer isIncomplete={isIncomplete} language={language}>
<CodeBlockHeader language={language} />
<div ref={containerRef} className="overflow-hidden rounded-md" />
</CodeBlockContainer>
);
};Register it alongside other renderers:
import { Streamdown } from "streamdown";
import { InfographicRenderer } from "./infographic-renderer";
import { VegaLiteRenderer } from "./vega-lite-renderer";
<Streamdown
plugins={{
renderers: [
{ language: ["vega", "vega-lite"], component: VegaLiteRenderer },
{ language: "infographic", component: InfographicRenderer },
],
}}
>
{markdown}
</Streamdown>Your AI can then output infographics using the AntV DSL:
```infographic
infographic list-row-horizontal-icon-arrow
data
title Product Development Lifecycle
desc Complete process from requirements to launch
items
- label Research
value 15
desc User interviews and competitive analysis
icon mdi/account-search
- label Design
value 42
desc Interaction prototype and visual design
icon mdi/palette
- label Development
value 65
desc Implementation and testing
icon mdi/code-tags
- label Launch
value 100
desc Official release and user feedback
icon mdi/rocket-launch
```Since AntV Infographic supports streaming natively, you can skip the isIncomplete guard and render progressively as the code fence streams in.
D2 diagrams
D2 is a declarative diagramming language. Since D2 compiles to SVG server-side, you can render it with an API call:
import { useEffect, useRef } from "react";
import type { CustomRendererProps } from "streamdown";
import { CodeBlockContainer, CodeBlockHeader } from "streamdown";
export const D2Renderer = ({
code,
language,
isIncomplete,
}: CustomRendererProps) => {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isIncomplete || !containerRef.current) {
return;
}
let cancelled = false;
const render = async () => {
const response = await fetch("/api/d2", {
method: "POST",
body: code,
});
if (cancelled || !containerRef.current) {
return;
}
const svg = await response.text();
containerRef.current.innerHTML = 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 rounded-md bg-muted">
<span className="text-muted-foreground text-sm">
Loading diagram...
</span>
</div>
) : (
<div ref={containerRef} className="overflow-hidden rounded-md p-4" />
)}
</CodeBlockContainer>
);
};During streaming, isIncomplete is true while the code fence is still being written. For most renderers, show a loading placeholder and wait for the complete spec:
const MyRenderer = ({ code, isIncomplete }: CustomRendererProps) => {
if (isIncomplete) {
return (
<div className="flex h-48 items-center justify-center rounded-md bg-muted">
<span className="text-muted-foreground text-sm">Loading...</span>
</div>
);
}
return <MyVisualization data={code} />;
};Some libraries like AntV Infographic support progressive rendering — in those cases, you can render on every update and skip the isIncomplete guard.
Custom renderers work alongside all other plugins. The rendering priority is:
- Custom renderers (checked first)
- Mermaid plugin (if configured)
- Default code block with syntax highlighting
import { mermaid } from "@streamdown/mermaid";
import { code } from "@streamdown/code";
<Streamdown
plugins={{
code,
mermaid,
renderers: [
{ language: "vega-lite", component: VegaLiteRenderer },
{ language: "infographic", component: InfographicRenderer },
],
}}
>
{markdown}
</Streamdown>interface CustomRendererProps {
code: string;
language: string;
isIncomplete: boolean;
}
interface CustomRenderer {
language: string | string[];
component: React.ComponentType<CustomRendererProps>;
}
interface PluginConfig {
// ...existing fields
renderers?: CustomRenderer[];
}





























