---
title: Syntax highlighting
description: Add optional Shiki highlighting, language labels, and copy controls with @smoothstream/code.
---

Syntax highlighting is a separate package so applications that do not need Shiki do not download it. Without a highlighter, fenced blocks render as ordinary `<pre><code>` with no toolbar.

Install `@smoothstream/code` next to your adapter and pass `codeHighlighter`.

<CodeGroup>
```bash title="npm" hideLanguageIcon
npm install @smoothstream/code
```

```bash title="pnpm" hideLanguageIcon
pnpm add @smoothstream/code
```

```bash title="yarn" hideLanguageIcon
yarn add @smoothstream/code
```

```bash title="bun" hideLanguageIcon
bun add @smoothstream/code
```
</CodeGroup>

<Tabs>
  <Tab label="React">
    ```tsx
    import { Smoothstream } from "@smoothstream/react";
    import { codeHighlighter } from "@smoothstream/code";

    <Smoothstream codeHighlighter={codeHighlighter} receiving={receiving}>
      {text}
    </Smoothstream>
    ```
  </Tab>
  <Tab label="Vue">
    ```vue
    <script setup lang="ts">
    import { Smoothstream } from "@smoothstream/vue";
    import { codeHighlighter } from "@smoothstream/code";
    </script>

    <template>
      <Smoothstream
        :markdown="text"
        :receiving="receiving"
        :code-highlighter="codeHighlighter"
      />
    </template>
    ```
  </Tab>
  <Tab label="Vanilla">
    ```ts
    import { createSmoothstream } from "@smoothstream/dom";
    import { codeHighlighter } from "@smoothstream/code";

    createSmoothstream(container, {
      receiving: true,
      codeHighlighter,
    });
    ```
  </Tab>
</Tabs>

Attaching a highlighter also opts fenced blocks into Smoothstream's code surface: a language label and a copy button. The copy control mounts only after the closing fence is known. It copies the full raw code even if the visual reveal is still running.

## Choose a theme

`codeHighlighter` is a ready-made instance using Shiki's `github-light` theme. Create your own for a different bundled theme, hidden labels, or paired light and dark themes.

```ts
import { createCodeHighlighter } from "@smoothstream/code";

const codeHighlighter = createCodeHighlighter({
  theme: "vitesse-light",
});
```

```ts
const codeHighlighter = createCodeHighlighter({
  showLanguageLabels: false,
});
```

`theme` and `themes` are mutually exclusive. For applications with light and dark color schemes, pass Shiki's `themes` API:

```ts
const codeHighlighter = createCodeHighlighter({
  themes: {
    light: "vitesse-light",
    dark: "vitesse-dark",
  },
  defaultColor: "light-dark()",
});
```

`defaultColor` follows Shiki: `"light"` (default), `"dark"`, `"light-dark()"`, or `false`. The `light-dark()` strategy follows the nearest CSS `color-scheme`. Class-based toggles should update that property:

```css
html {
  color-scheme: light;
}

html.dark {
  color-scheme: dark;
}
```

Token styles also keep Shiki's `--shiki-light` and `--shiki-dark` variables if you prefer selector-based switching.

<Callout type="info" title="What loads in the browser">
  Bundled themes and grammars are lazy. The app can ship the Shiki registry, but the browser fetches only the selected theme and the languages Smoothstream actually encounters. Unknown fence languages keep the theme palette and fall back to unhighlighted code instead of blocking playback. Line colors become immutable before the first character of that line is revealed.
</Callout>
