---
title: Custom renderers
description: Register custom renderers for arbitrary code fence languages.
type: reference
summary: Map code fence languages to custom React components for rendering charts, diagrams, and more.
prerequisites:
  - /docs/configuration
related:
  - /docs/plugins/mermaid
  - /docs/code-blocks
  - /docs/plugins
---

# Custom renderers



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.

## Usage

Pass an array of `{ language, component }` objects to `plugins.renderers`:

```tsx title="chat.tsx" lineNumbers
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>
  );
}
```

## Multiple languages

The `language` field accepts a string or an array of strings:

```tsx
const renderers = [
  { language: ["vega", "vega-lite"], component: VegaLiteRenderer },
  { language: "infographic", component: InfographicRenderer },
  { language: "d2", component: D2Renderer },
];

<Streamdown plugins={{ renderers }}>{markdown}</Streamdown>
```

## Custom renderer props

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 |

## Reusing built-in components

Streamdown exports its internal code block components so your custom renderers can reuse them for consistent styling:

```tsx title="vega-lite-renderer.tsx" lineNumbers
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                          |

## Examples

### Vega-Lite charts

[Vega-Lite](https://vega.github.io/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`:

```bash
npm install vega vega-lite vega-embed
```

The renderer from the [reusing built-in components](#reusing-built-in-components) section above is a complete Vega-Lite implementation. Once registered, your AI can output charts like:

````markdown
```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](https://github.com/antvis/Infographic) renders rich infographic diagrams from a YAML-like DSL that supports streaming output. Install the package, then create a renderer:

```bash
npm install @antv/infographic
```

```tsx title="infographic-renderer.tsx" lineNumbers
import { 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:

```tsx title="chat.tsx" lineNumbers
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:

````markdown
```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](https://d2lang.com/) is a declarative diagramming language. Since D2 compiles to SVG server-side, you can render it with an API call:

```tsx title="d2-renderer.tsx" lineNumbers
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>
  );
};
```

## Streaming considerations

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:

```tsx
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.

## Combining with other plugins

Custom renderers work alongside all other plugins. The rendering priority is:

1. Custom renderers (checked first)
2. Mermaid plugin (if configured)
3. Default code block with syntax highlighting

```tsx
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>
```

## Type reference

```tsx
interface CustomRendererProps {
  code: string;
  language: string;
  isIncomplete: boolean;
}

interface CustomRenderer {
  language: string | string[];
  component: React.ComponentType<CustomRendererProps>;
}

interface PluginConfig {
  // ...existing fields
  renderers?: CustomRenderer[];
}
```
