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"
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.