Skip to content

Class-based output

Nested scope classes make simple styling a few lines of CSS while keeping ancestor context, scope order, and repeated scopes available for precise rules. Inline styles stay the default for codeToHtml; class output is one option away.

The idea was inspired by GitHub's PrettyLights and wooorm's starry-night, which recreates PrettyLights-style class-based highlighting. Ferriki implements this direction on its native TextMate runtime, retaining full scope paths and extracting CSS from resolved theme styles.

"hello"
{"message":"hello", "escaped":"line\n"}

Change the theme or enable the CSS override. The highlighted token elements stay in place.

Use your own CSS

import { codeToHtmlWithCss } from "@ferriki/core";

const { html, css } = await codeToHtmlWithCss('const message = "hello";', {
  lang: "javascript",
  theme: "github-dark",
});
.ferriki .tok-string {
  color: #0a3069;
}
.ferriki .tok-keyword,
.ferriki .tok-storage {
  color: #cf222e;
}
.ferriki .tok-comment {
  color: #57606a;
  font-style: italic;
}

tok-* classes include every scope prefix in a token's path. leaf-* classes identify its innermost scope. Nested scope-* wrappers preserve context:

.ferriki .scope-meta-structure-dictionary-json .leaf-string-quoted-double-json {
  color: #8250df;
}

Dots become hyphens, and literal hyphens become _2d_. For example, a JSON property name has .tok-support-type-property_2d_name-json. exact-* wrappers identify complete scope names when you need child combinators for exact order or repetition. Names come from the grammar and can vary between languages.

Keep your existing themes

TextMate/Shiki themes need no changes. Ferriki resolves their context and priority rules, then moves the resulting styles into generated CSS classes:

import { codeToHtmlWithCss } from "@ferriki/core";

const { html, css } = await codeToHtmlWithCss('{"message":"hello"}', {
  lang: "json",
  theme: "monokai",
});

Add html to the page and css to your stylesheet. Combine the CSS from every rendered block; it covers those blocks, rather than arbitrary future code. Generated style class names are internal. Use readable scope classes for your own rules: generated declarations have zero specificity and are easy to override.

Monokai colors a standalone JSON "hello" differently from a string inside a dictionary. The example above retains that distinction. Enable the override to apply .tok-string to both contexts.

Switch themes with CSS

const { html, css } = await codeToHtmlWithCss(source, {
  lang: "typescript",
  themes: { light: "github-light", dark: "monokai" },
});

Set data-ferriki-theme="dark" on the code block or an ancestor. Switch to light to switch back. The generated CSS includes both palettes: the browser loads no theme JSON and does not tokenize again. A root attribute takes precedence over an ancestor. defaultColor: "light-dark()" follows the system preference until you select a theme explicitly.

For custom CSS themes, use theme: "none" and your site's normal classes, attributes, or media queries. There is no theme format to adopt.

The default React <Code /> presentation accepts semantic color custom properties inherited from normal page CSS. Category-specific variables recolor their category and any child categories that fall back through them. When category variables are unset, semantic tokens keep their resolved theme colors. --ferriki-foreground sets the code block's default text color, while --ferriki-background-color controls its background; the foreground variable does not flatten themed token colors. The Vite macro retains native scope boundaries during inline build-time preparation to classify semantic tokens. This does not change the general inline codeToHtml() output contract. Use nested scope classes when a rule depends on a token's grammar context; the semantic palette cannot express every nested rule. See the Shadow code elements guide for the variable reference and examples.

Integration details

Classes mode uses a .ferriki root instead of .shiki and preserves grammar boundaries, so whitespace and equal-style token merging are skipped. Transformers and decorations still run; callbacks operate on source tokens before scopes are nested and styles are extracted. Inline structure uses a code.ferriki wrapper. Transformers that assume the default flat HTML may need adaptation.

See the complete class guide for the class contract, CSS collection, custom variable prefixes, grammar-state continuation, and the Rust API. The evaluation report records the tested corpus and the limits of that evidence.