Use @smoothstream/code 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.
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:
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.
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 already supports paired themes; CSS variables can override the code surface.