CSS Theme Files
Use Lumis CSS theme files with htmlLinked output in JavaScript / TypeScript, Elixir, and CDN workflows.
The htmlLinked formatter emits CSS class names instead of inline styles. You provide a theme stylesheet.
JavaScript / TypeScript package
Import a stylesheet from @lumis-sh/themes/css/<theme>.css.
import '@lumis-sh/themes/css/catppuccin_frappe.css'
import {highlight} from '@lumis-sh/lumis'
import {htmlLinked} from '@lumis-sh/lumis/formatters'
import javascript from '@lumis-sh/wasm-javascript'
const html = await highlight(
'const x = 1',
htmlLinked({language: javascript}),
)CDN
Load the compiled CSS file directly:
<link
rel="stylesheet"
href="https://unpkg.com/@lumis-sh/themes/dist/css/catppuccin_frappe.css"
>Available distribution folders:
- CSS:
https://unpkg.com/browse/@lumis-sh/themes/dist/css/ - JSON:
https://unpkg.com/browse/@lumis-sh/themes/dist/json/
Elixir / Phoenix
Lumis ships CSS files under priv/static/css/.
plug Plug.Static,
at: "/themes",
from: {:lumis, "priv/static/css/"},
only: ["catppuccin_frappe.css"]<link
phx-track-static
rel="stylesheet"
href={~p"/themes/catppuccin_frappe.css"}
/>Repository-generated CSS
Inside this repo, CSS theme files are generated into multiple targets:
css/*.csspackages/elixir/lumis/priv/static/css/packages/javascript/themes/dist/css/
That is why the same theme can be consumed from Rust assets, Elixir static files, and the npm themes package.
When to use CSS theme files
- many code blocks on one page
- existing app stylesheet pipeline
- cacheable stylesheets
- smaller HTML output than inline styles
If you want the block to be self-contained instead, use htmlInline.
Token classes
Token classes use the l- prefix. Dots in Tree-sitter highlight scopes become
dashes and the rest of the scope is left alone, so comment becomes .l-comment,
tag.attribute becomes .l-tag-attribute, and attribute.c_sharp becomes
.l-attribute-c_sharp.
The gutter is not a Tree-sitter scope and does not follow that rule: LineNr is
.l-line-number and CursorLineNr is .l-line-number-highlighted, which a
highlighted gutter carries alongside .l-line-number. Theme CSS applies
CursorLineNr without merging in LineNr, and a theme without CursorLineNr
falls back to LineNr.
The block and language classes stay bare: .lumis and language-*.
.l-comment {
font-style: normal;
}If the pre-built files do not fit, generate a stylesheet at runtime with the CSS Builder. Use it for parent selectors, custom block selectors, or a background that comes from your app CSS.
Line layout
Each theme file ends with a small @layer lumis block that lays out the lines.
The CSS that CssBuilder, build_css and buildCss generate ends with the same
block by default. To generate theme colors without these rules, set
layout: false or use
lumis themes build-css <theme> --no-layout:
@layer lumis {
pre.lumis > code {
display: block;
width: fit-content;
min-width: 100%;
overflow-wrap: anywhere;
}
pre.lumis > code > span.l-line {
display: inline-block;
width: 100%;
min-height: 1lh;
vertical-align: top;
}
.l-line-number {
-webkit-user-select: none;
user-select: none;
}
}<code> grows to its longest line, so a highlighted line covers the whole
scrolled width, and a block wrapped with white-space: pre-wrap still wraps at
its container. min-height keeps an empty line one row tall, and
vertical-align: top stops it from sitting on the baseline and stretching the
row. Line numbers stay out of selection copy.
If your stylesheet targets lines:
- Select lines with
.l-line; each line is a<span>. - Keep lines inline or inline-block. A
display: blockline plus the newline after it renders an extra blank row, andcode { display: grid }makes some engines leave empty lines out of copied text. - Add
box-sizing: border-boxwhen a line has horizontal padding or a border, so it still fits inwidth: 100%.
Your own CSS overrides the block as long as it is not in a layer itself. If it
is, as with Tailwind v4, declare lumis before your layers; see
Tailwind CSS.
If your styles target div.l-line, follow the
HTML line markup migration.