---
title: Components
description: Learn how to customize and extend Streamdown with custom component overrides.
url: "https://streamdown.ai/docs/components"
docs_index: /llms.txt
lastUpdated: 2026-10-19
type: reference
summary: Override default HTML elements with custom React components for full rendering control.
prerequisites:
  - /docs/getting-started
related:
  - /docs/styling
  - /docs/plugins
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

Streamdown allows you to replace any Markdown element with your own React component while maintaining all of Streamdown's functionality.

## Basic Usage

Pass custom components using the `components` prop:

```tsx title="app/page.tsx"
<Streamdown
  components={{
    h1: ({ children }) => (
      <h1 className="text-4xl font-bold text-blue-600">
        {children}
      </h1>
    ),
    h2: ({ children }) => (
      <h2 className="text-3xl font-semibold text-blue-500">
        {children}
      </h2>
    ),
    p: ({ children }) => (
      <p className="text-gray-700 leading-relaxed">
        {children}
      </p>
    ),
  }}
>
  {markdown}
</Streamdown>
```

## Available Components

You can override any of the following standard HTML components:

- **Headings**: `h1`, `h2`, `h3`, `h4`, `h5`, `h6`
- **Text**: `p`, `strong`, `em`
- **Lists**: `ul`, `ol`, `li`
- **Links**: `a`
- **Code**: `code`, `pre`
- **Quotes**: `blockquote`
- **Tables**: `table`, `thead`, `tbody`, `tr`, `th`, `td`
- **Media**: `img`
- **Other**: `hr`, `sup`, `sub`, `section`

## Component Props

Custom components receive all the props that the default components would receive, including:

- `children` - The content to render
- `className` - CSS class names from the Markdown AST (if applicable). This does **not** include Streamdown's built-in default styles.
- `node` - The Markdown AST node (for advanced use cases)
- Element-specific props (e.g., `href` for links, `src` for images)

> Custom components **fully replace** the default implementations, including their built-in Tailwind styles. The `className` prop only contains classes from the Markdown AST (e.g., `language-js` on code elements) — it does **not** include the default styles that Streamdown normally applies.
>
> If you need to preserve the default appearance and behavior, import `defaultComponents` and render the built-in component inside your override. This keeps default styles and attributes like `data-streamdown` in sync with Streamdown updates.
>
> Prefer this over re-applying Tailwind class strings by hand. For style-only tweaks, you can also use [CSS selectors with `data-streamdown` attributes](/docs/styling#global-css-targeting).

```tsx title="app/page.tsx"
import { Streamdown, defaultComponents } from "streamdown";

const DefaultH2 = defaultComponents.h2;

export default function Page({ markdown }: { markdown: string }) {
  return (
    <Streamdown
      components={{
        // ❌ Loses default spacing, font styles, and data-streamdown
        h2: ({ children }) => (
          <h2 className="text-blue-500">{children}</h2>
        ),
        // ✅ Composes on top of the built-in component
        h2: (props) => (
          <DefaultH2 {...props} className={`${props.className ?? ""} text-blue-500`} />
        ),
      }}
    >
      {markdown}
    </Streamdown>
  );
}
```

You can also wrap defaults for conditional behavior:

```tsx title="app/page.tsx"
import { createElement, type ComponentProps } from "react";
import { Streamdown, defaultComponents, type ExtraProps } from "streamdown";

function AOverride(props: ComponentProps<"a"> & ExtraProps) {
  if (props.href?.startsWith("/internal")) {
    return <a {...props} className="text-emerald-600" />;
  }

  return createElement(defaultComponents.a, props);
}

export default function Page({ markdown }: { markdown: string }) {
  return (
    <Streamdown components={{ a: AOverride }}>
      {markdown}
    </Streamdown>
  );
}
```

## Inline Code

When you override `components.code`, you replace the entire code rendering pipeline — inline code, block code with syntax highlighting, mermaid diagrams, and custom renderers. To customize only inline code without affecting block code, use the `inlineCode` virtual component:

```tsx title="app/page.tsx"
<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>
```

Block code blocks, syntax highlighting, and mermaid diagrams continue to work normally. You can also combine `inlineCode` with a custom `code` component — `inlineCode` handles inline spans while `code` handles fenced code blocks:

```tsx title="app/page.tsx"
<Streamdown
  components={{
    inlineCode: ({ children }) => (
      <code className="bg-violet-100 text-violet-800 rounded px-1 text-sm">
        {children}
      </code>
    ),
    code: MyCustomCodeBlock,
  }}
>
  {markdown}
</Streamdown>
```

## Streaming State

When streaming markdown, custom components can detect if their code fence is still being streamed using the `useIsCodeFenceIncomplete` hook. This is useful for expensive-to-render components where you want to show a loading state until the code block is complete.

```tsx title="app/page.tsx"
import { Streamdown, useIsCodeFenceIncomplete } from "streamdown";

const MyCodeBlock = ({ children }) => {
  const isIncomplete = useIsCodeFenceIncomplete();

  if (isIncomplete) {
    return <div className="animate-pulse bg-muted h-24 rounded" />;
  }

  return <pre><code>{children}</code></pre>;
};

export default function Page() {
  return (
    <Streamdown
      components={{ code: MyCodeBlock }}
      isAnimating={isStreaming}
    >
      {markdown}
    </Streamdown>
  );
}
```

The hook returns `true` when all of the following are true:

- `isAnimating={true}` (streaming mode is active)
- The component is in the last block being streamed
- That block has an unclosed code fence (` ``` ` without a closing ` ``` `)

Once the code fence closes, the hook returns `false` and your component can render normally—even while the rest of the markdown continues streaming.

> This is particularly useful for Mermaid diagrams and syntax highlighters where continuous re-rendering during streaming would cause performance issues.

```tsx title="app/page.tsx"
<Streamdown
  components={{
    a: ({ href, children, ...props }) => (
      <a
        href={href}
        className="text-purple-600 hover:text-purple-800 underline"
        {...props}
      >
        {children}
      </a>
    ),
  }}
>
  {markdown}
</Streamdown>
```

## Preserving Table Interactivity

When overriding the `table` component, the built-in copy and download buttons are lost because custom components fully replace default implementations. To restore them, import the table action components and include them in your custom table:

```tsx title="app/page.tsx"
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()` to find this wrapper, then `.querySelector("table")` to locate the `<table>` element inside it. Your custom table component must render a `<table>` element as a descendant of the wrapper div.

A `TableDownloadButton` component is also available for rendering a single-format download button instead of a dropdown:

```tsx title="app/page.tsx"
import { TableDownloadButton } from "streamdown";

<TableDownloadButton format="csv" />
```

You can control the CSV delimiter for table copy and download with `controls.table.csvSeparator`. Supported values are `","`, `";"`, `"\t"`, and `"auto"` (locale-aware selection). The default is `","`.

```tsx title="app/page.tsx"
<Streamdown controls={{ table: { csvSeparator: "auto" } }}>
  {markdown}
</Streamdown>
```

### Lower-level utilities

For fully custom implementations, use the extraction and conversion utilities directly:

```tsx title="app/page.tsx"
import {
  extractTableDataFromElement,
  type CSVSeparator,
  tableDataToCSV,
  tableDataToTSV,
  tableDataToMarkdown,
} from "streamdown";

// Extract structured data from a <table> DOM element
const data = extractTableDataFromElement(tableElement);

// Convert to various formats
const csv = tableDataToCSV(data, "auto");
const tsv = tableDataToTSV(data);
const markdown = tableDataToMarkdown(data);
```

The `tableDataToCSV` helper accepts an optional `CSVSeparator` argument (`"," | ";" | "\t" | "auto"`) so you can choose the delimiter explicitly or let `"auto"` pick a locale-friendly separator.

## Custom HTML Tags

You can render custom HTML tags from AI responses (like `<source>`, `<mention>`, etc.) using the `allowedTags` prop alongside `components`. This is useful when you instruct the AI to output structured data that renders as interactive components.

For example, you might add a system prompt:

```
When referencing a source, use: <source id="123">Source Title</source>
```

The AI then outputs markdown containing:

```markdown
According to the documentation <source id="abc">Getting Started Guide</source>, you should...
```

### Setup

Use the `allowedTags` prop to specify which custom tags and attributes to allow through sanitization, then map them to React components:

```tsx title="app/page.tsx"
<Streamdown
  allowedTags={{
    source: ["id"],  // Allow <source> tag with id attribute
  }}
  components={{
    source: ({ id, children }) => (
      <button
        onClick={() => console.log(`Navigate to source: ${id}`)}
        className="text-blue-600 underline cursor-pointer"
      >
        {children}
      </button>
    ),
  }}
>
  {markdown}
</Streamdown>
```

### Multiple Custom Tags

You can allow multiple custom tags:

```tsx title="app/page.tsx"
<Streamdown
  allowedTags={{
    source: ["id"],
    mention: ["user_id", "type"],
    action: ["name", "payload"],
  }}
  components={{
    source: ({ id, children }) => (
      <SourceBadge sourceId={id as string}>{children}</SourceBadge>
    ),
    mention: ({ user_id, children }) => (
      <UserMention userId={user_id as string}>{children}</UserMention>
    ),
    action: ({ name, payload, children }) => (
      <ActionButton name={name as string} payload={payload as string}>
        {children}
      </ActionButton>
    ),
  }}
>
  {markdown}
</Streamdown>
```

### Fallback for missing map entries

When you only need a shared renderer for tags that are **not** already in the
built-in map (or your `components` overrides), use `fallbackComponent` instead of
enumerating every tag:

```tsx title="app/page.tsx"
import { createElement } from "react";

<Streamdown
  allowedTags={{
    mention: ["user_id"],
    chip: [],
  }}
  fallbackComponent={({ node, children, ...props }) =>
    createElement(node!.tagName, props, children)
  }
  components={{
    // Still wins over fallbackComponent for this tag
    mention: ({ user_id, children }) => (
      <UserMention userId={user_id as string}>{children}</UserMention>
    ),
  }}
>
  {markdown}
</Streamdown>
```

`fallbackComponent` also applies to standard HTML tags absent from both the
built-in set and `components` (for example `<span>`, `<em>`, `<div>`, `<br>`).

> This is **not** a full unstyled mode. Built-in renderers (`h1`, `p`, `code`,
> …) and any explicit `components` entries always take precedence. To restyle
> those tags, override them in `components`.

### Data Attributes

Use `data*` in the attributes array to allow all `data-*` attributes on a tag:

```tsx title="app/page.tsx"
<Streamdown
  allowedTags={{
    widget: ["data*"],  // Allow all data-* attributes
  }}
  components={{
    widget: (props) => <Widget {...props} />,
  }}
>
  {markdown}
</Streamdown>
```

### Important Notes

- Without `allowedTags`, custom tags are stripped by the sanitizer (content is preserved, tags are removed)
- Only attributes listed in `allowedTags` are preserved; unlisted attributes are stripped
- The `allowedTags` prop only works with the default rehype plugins

> If you provide custom `rehypePlugins`, you'll need to configure `rehype-sanitize` yourself to allow custom tags. See the [Security](/docs/security) documentation for details.

### Plain text tag content

By default, children of custom HTML tags are parsed as markdown. This means underscores, asterisks, and other markdown metacharacters in tag content get formatted unexpectedly — for example, `<mention>some_user_name</mention>` renders with italicized text instead of a literal underscore.

Use the `literalTagContent` prop to treat the children of specific tags as plain text:

```tsx title="app/page.tsx"
<Streamdown
  allowedTags={{
    mention: ["user_id"],
  }}
  literalTagContent={["mention"]}
  components={{
    mention: ({ user_id, children }) => (
      <span className="text-blue-600">@{children}</span>
    ),
  }}
>
  {markdown}
</Streamdown>
```

> Tags listed in `literalTagContent` must also be listed in `allowedTags`. Otherwise the tag is stripped by the sanitizer before literal content handling applies.

### Security Considerations

When allowing custom HTML tags:

- Only whitelist tags you explicitly need
- Only whitelist attributes you explicitly need
- Validate attribute values in your component before using them
- Never allow `script`, `style`, or event handler attributes (`onclick`, etc.)

See the [Security](/docs/security) documentation for more details on HTML handling.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)