Guides

Expressive Code

satteri-expressive-code renders fenced code blocks with Expressive Code: syntax highlighting through Shiki, editor and terminal frames, line markers, and a copy button. It's a HAST plugin — the Sätteri equivalent of rehype-expressive-code.

Install

pnpm add satteri-expressive-code satteri
npm install satteri-expressive-code satteri
yarn add satteri-expressive-code satteri
bun add satteri-expressive-code satteri

satteri is a peer dependency, so install it alongside the plugin.

Usage

Pass the plugin to hastPlugins. Its visitor is async, so the compile returns a Promise:

import { markdownToHtml } from "satteri";
import expressiveCode from "satteri-expressive-code";

const { html } = await markdownToHtml(source, {
  hastPlugins: [expressiveCode({ themes: ["github-dark", "github-light"] })],
});

The CSS and JS that Expressive Code needs are injected into the rendered HTML once per document, so the output is self-contained.

Options

OptionTypeDefaultEffect
themes(string | ExpressiveCodeTheme)[]Shiki theme names or theme objects. CSS variables are generated per theme.
tabWidthnumber2Spaces a tab expands to in code blocks; 0 keeps tabs.

When themes is omitted, Expressive Code's own defaults apply (currently github-dark and github-light).

expressiveCode extends ExpressiveCodeConfig, so every other Expressive Code option — styleOverrides, plugins, useDarkModeMediaQuery, themeCssSelector, and the rest — is accepted alongside the two above.

Themes

themes takes any mix of:

  • a bundled Shiki theme name (e.g. github-dark), loaded for you;
  • a theme object in VS Code / Shiki JSON format;
  • an ExpressiveCodeTheme instance.

Expressive Code generates a set of CSS variables per theme. Pass exactly one dark and one light theme and it also emits a prefers-color-scheme media query by default — control that with useDarkModeMediaQuery and themeCssSelector.

expressiveCode({ themes: ["github-light", "github-dark"] });

Advanced

Three optional hooks customise rendering. The two per-block hooks receive the block input and a document ({ source, filename }); all three may also return a Promise.

HookTypeEffect
getBlockLocale({ input, document }) => string | undefinedSet a per-block locale, for multi-language sites.
customCreateBlock({ input, document }) => ExpressiveCodeBlockReplace how each ExpressiveCodeBlock is constructed.
customCreateRenderer(options) => SatteriExpressiveCodeRendererReplace the renderer. Its result is cached and reused per document.

customCreateRenderer returns the same shape as the exported createRenderer, which builds the ExpressiveCode instance (loading any Shiki themes) and the assets to inject:

createRenderer(options?: SatteriExpressiveCodeOptions): Promise<SatteriExpressiveCodeRenderer>;

interface SatteriExpressiveCodeRenderer {
  ec: ExpressiveCode;
  baseStyles: string;
  themeStyles: string;
  jsModules: string[];
}

Re-exports

Everything from expressive-code is re-exported from satteri-expressive-code, so its themes and helpers are importable from one place. HAST types live at the satteri-expressive-code/hast subpath.

See the Plugins guide and the Plugin API reference.