---
title: Custom highlighters
description: Implement the CodeHighlighter contract for your own tokenizer, theme loading, or shared asset infrastructure.
---

Use [`@smoothstream/code`](/customize/syntax-highlighting) for the standard Shiki integration. Supply a custom highlighter when your application needs a different tokenizer or controls how grammars and themes are loaded, such as serving shared assets from a CDN.

The `codeHighlighter` option accepts the same framework-neutral contract in every adapter. A custom implementation does not need the `@smoothstream/code` package. Smoothstream continues to own line admission, reveal timing, and the code toolbar.

## Implement the contract

This minimal implementation returns plain tokens to illustrate the result shape. It adds a palette and language label without applying syntax colors. Replace the line tokenization with your own highlighter.

```ts title="plain-highlighter.ts"
import type { CodeHighlighter } from "@smoothstream/core";

export const plainHighlighter: CodeHighlighter = {
  name: "app/plain",
  async highlight({ code, language }) {
    // Requests contain complete lines with a terminating newline.
    const lines = code.split("\n").slice(0, -1);
    return {
      languageLabel: language === "text" ? "Plain text" : language,
      palette: {
        backgroundColor: "#f6f8fa",
        color: "#24292f",
      },
      lines: lines.map((line) => ({
        tokens: line === "" ? [] : [{ content: line }],
      })),
    };
  },
};
```

The contract types are exported by `@smoothstream/core` and re-exported by `@smoothstream/react` and `@smoothstream/vue`. You can use your framework package's type export instead of importing core directly. If your project imports core directly, declare it as a dependency rather than relying on package-manager hoisting.

Pass the instance through the usual adapter option:

<Tabs>
  <Tab label="React" icon="/icons/react.svg">
    ```tsx
    <Smoothstream codeHighlighter={plainHighlighter} receiving={receiving}>
      {text}
    </Smoothstream>
    ```
  </Tab>
  <Tab label="Vue" icon="/icons/vue.svg">
    ```vue
    <Smoothstream
      :markdown="text"
      :receiving="receiving"
      :code-highlighter="plainHighlighter"
    />
    ```
  </Tab>
  <Tab label="Vanilla DOM" icon="/icons/javascript-typescript.svg">
    ```ts
    const stream = createSmoothstream(container, {
      codeHighlighter: plainHighlighter,
      receiving: true,
    });
    ```
  </Tab>
</Tabs>

Create the highlighter outside repeated renders and reuse it. If it keeps per-block state, use the supplied session identity to separate different fences and different response instances.

## Requests

`highlight(request)` can return a result synchronously or return a promise.

| Field | Meaning |
| --- | --- |
| `code` | All currently committed lines for this block, including their terminating newlines. This is accumulated code, not a delta. |
| `language` | The fence's language identifier, or `text` when no language is specified. Handle aliases and normalization your tokenizer needs. |
| `session` | An opaque object stable for this fenced block. It distinguishes blocks even when their code is identical. |

Do not key mutable tokenizer state by the code string or language alone. A `WeakMap` keyed by `request.session` can hold the previously processed source, tokenizer state, and a queue for that block. Later calls may arrive while earlier asynchronous work is still running, so serialize operations if the tokenizer mutates its state.

`supportsLanguage?(language)` is an optional method on the contract. Current adapters still call `highlight` for requested languages; handle unknown languages in `highlight` itself. The bundled extension returns plain tokens for them.

## Results

| Field | Meaning |
| --- | --- |
| `lines` | One entry per requested line, in source order. Each entry has a `tokens` array. |
| `tokens[].content` | Exact text for that token, without the line's newline. All tokens for a line must concatenate to the original line. |
| `tokens[].style` | Optional style properties such as `color`, `fontStyle`, or CSS custom properties. |
| `languageLabel` | Optional display label for the code toolbar. |
| `palette` | Optional `backgroundColor`, `color`, and `style` for the code surface. |

Return token data, not an HTML string. An empty line can use `tokens: []`. Styles are trusted presentation values supplied by your application; do not copy arbitrary model-generated objects into them.

## Keep accepted lines stable

Smoothstream preserves previously accepted lines as a block grows. It also retains the initial accepted palette and language label. This prevents colors and toolbar content from changing underneath an ongoing reveal.

Return only when you can provide stable tokenization for the complete requested lines. For example, a tokenizer with multiline grammar state should carry that state forward instead of highlighting each line as an isolated document.

<Callout type="warning" title="Missing lines do not stay pending">
  Missing lines or tokens that fail to reconstruct the source line are replaced with plain tokens. That fallback can become the accepted presentation for the line. Omitting a line is not a way to defer it; keep the result pending until it is ready.
</Callout>

A rejected highlight promise falls back to plain code. Use an `async` implementation when loading or tokenization can fail, so failures become promise rejections. There is no built-in timeout: a promise that never settles can hold the affected code and later scheduled content. Handle timeouts and fallback in your implementation when needed.

The adapters discard superseded asynchronous results, but that does not undo mutations inside your tokenizer. Keep per-session work ordered even when the renderer no longer needs an earlier result.

## Toolbar and theme behavior

Attaching a custom highlighter enables the same code-copy surface as the bundled extension. It does not require Shiki. Return `languageLabel` for a readable title, or set `showLanguageLabels: false` on the highlighter to hide labels while retaining copy.

The copy control uses canonical source code, not token HTML, and becomes available when the block is confirmed complete. Your highlighter does not need to implement clipboard handling.

For light and dark themes, return token styles and palette values that can respond to CSS, such as CSS custom properties or `light-dark()` values. Changing the entire highlighter during a response is not a theme-switching strategy. The [bundled extension](/customize/syntax-highlighting#choose-a-theme) already supports paired themes; [CSS variables](/reference/css-variables) can override the code surface.
