Components

Learn how to customize and extend Streamdown with custom component overrides.

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:

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)
  • 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, you must re-apply the styles yourself. See the Styling documentation for the default classes, or use CSS selectors with data-streamdown attributes instead of component overrides when you only need visual changes.

For example, the default h2 component applies mt-6 mb-2 font-semibold text-2xl. When you override it, those styles are lost unless you include them:

app/page.tsx
<Streamdown
  components={{
    // ❌ Loses default spacing and font styles
    h2: ({ children }) => (
      <h2 className="text-blue-500">{children}</h2>
    ),
    // ✅ Preserves default styles alongside custom ones
    h2: ({ children, className }) => (
      <h2 className={`mt-6 mb-2 font-semibold text-2xl text-blue-500 ${className ?? ''}`}>
        {children}
      </h2>
    ),
  }}
>
  {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.

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.

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>

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:

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:

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:

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>

Data Attributes

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

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 documentation for details.

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 documentation for more details on HTML handling.