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.
Popular themes
| Light | Dark |
|---|
github-light | github-dark |
catppuccin-latte | catppuccin-mocha |
vitesse-light | vitesse-dark |
slack-ochin | slack-dark |
one-light | one-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}).
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.
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:
| Component | Description |
|---|
CodeBlock | Full code block with highlighting and controls |
CodeBlockContainer | Outer wrapper with border and rounded corners |
CodeBlockHeader | Language label header bar |
CodeBlockCopyButton | Standalone copy-to-clipboard button |
CodeBlockDownloadButton | Standalone download button |
CodeBlockSkeleton | Loading 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>