Skip to main content
Streamdown renders code blocks with a polished UI including a language label, line numbers, copy and download buttons, and optional syntax highlighting via the @streamdown/code plugin.

Basic rendering

Code blocks render out of the box without any plugins. Use triple backticks with a language identifier:
```typescript
function greet(name: string): string {
  return `Hello, ${name}!`;
}
```
Without the code plugin, syntax highlighting is not applied but the block still renders with the header, line numbers, and controls.

Syntax highlighting

Syntax highlighting requires the @streamdown/code plugin, which uses Shiki under the hood.
npm install @streamdown/code
import { Streamdown } from "streamdown";
import { code } from "@streamdown/code";

export default function Page() {
  return (
    <Streamdown plugins={{ code }}>
      {markdown}
    </Streamdown>
  );
}
Language grammars are lazy-loaded on demand — only grammars for languages that actually appear in the rendered markdown are downloaded.

Theme configuration

Streamdown uses a dual-theme model: one theme for light mode and one for dark mode. The default themes are github-light and github-dark. Override them with the shikiTheme prop:
<Streamdown
  plugins={{ code }}
  shikiTheme={["catppuccin-latte", "catppuccin-mocha"]}
>
  {markdown}
</Streamdown>
The prop accepts [ThemeInput, ThemeInput] where ThemeInput is either a bundled theme name (BundledTheme) or a custom theme object (ThemeRegistrationAny):
import myDarkTheme from "./my-dark-theme.json";

<Streamdown
  plugins={{ code }}
  shikiTheme={["github-light", myDarkTheme]}
>
  {markdown}
</Streamdown>
When using the @streamdown/code plugin, the theme is read from the plugin’s own configuration via plugins.code.getThemes(). The shikiTheme prop is only used when the plugin is absent.
LightDark
github-lightgithub-dark
catppuccin-lattecatppuccin-mocha
vitesse-lightvitesse-dark
slack-ochinslack-dark
one-lightone-dark-pro
All Shiki bundled themes are supported.

Line numbers

Line numbers are shown by default on all code blocks.

Disable globally

<Streamdown lineNumbers={false}>{markdown}</Streamdown>

Disable per block

Add noLineNumbers to the code fence meta string:
```typescript noLineNumbers
const x = await fetchUser(id);
```

Custom start line

Set startLine=N in the meta string to begin numbering from a specific line:
```python startLine=42
def handle_request(req):
    return Response(200)
```

Controls

The controls prop configures which action buttons appear on code blocks. Controls are enabled by default (controls={true}).

Copy button

Copies the raw code to the clipboard. Shows a checkmark for 2 seconds after a successful copy. Automatically disabled when isAnimating={true} to prevent copying incomplete code.

Download button

Downloads the code as a file. The file extension is derived from the language identifier (e.g., file.ts for TypeScript).

Disabling controls

// Disable the download button only
<Streamdown controls={{ code: { download: false } }}>
  {markdown}
</Streamdown>

// Disable the copy button only
<Streamdown controls={{ code: { copy: false } }}>
  {markdown}
</Streamdown>

// Disable all code block controls
<Streamdown controls={{ code: false }}>
  {markdown}
</Streamdown>

// Disable all controls (code, table, mermaid)
<Streamdown controls={false}>
  {markdown}
</Streamdown>
See Interactivity for the full ControlsConfig reference.

Exported components

Streamdown exports its internal code block components for use in custom renderers:
ComponentDescription
CodeBlockFull code block with highlighting and controls
CodeBlockContainerOuter wrapper with border and rounded corners
CodeBlockHeaderLanguage label header bar
CodeBlockCopyButtonStandalone copy-to-clipboard button
CodeBlockDownloadButtonStandalone download button
CodeBlockSkeletonLoading skeleton placeholder
import {
  CodeBlock,
  CodeBlockContainer,
  CodeBlockHeader,
  CodeBlockCopyButton,
  CodeBlockDownloadButton,
  CodeBlockSkeleton,
} from "streamdown";

Streaming behavior

Incomplete code fences

During streaming, a code fence may arrive without its closing triple backticks. Streamdown detects this condition using hasIncompleteCodeFence and sets isIncomplete={true} on the block. The block still renders — the raw content is visible — but:
  • The cursor caret is hidden (a caret inside a code block looks broken)
  • Copy and download buttons are disabled
  • Custom renderers receive isIncomplete={true} and can show a loading state

Progressive highlighting

Highlighting is applied asynchronously when the Shiki language grammar finishes loading. Code renders immediately as plain text, then colors are applied once highlighting is ready. This avoids blocking the first paint on grammar download.

Inline code

Inline code uses single backticks:
Use the `useState` hook to manage local state.
Inline code renders with a monospace font, subtle background, rounded corners, and compact padding. To provide a custom inline code component, use the inlineCode key in the components prop:
<Streamdown
  components={{
    inlineCode: ({ children, ...props }) => (
      <code className="my-inline-code" {...props}>
        {children}
      </code>
    ),
  }}
>
  {markdown}
</Streamdown>

Build docs developers (and LLMs) love