# CSS Theme Files (/themes/css-files)

The `htmlLinked` formatter emits CSS class names instead of inline styles. You provide a theme stylesheet.

<span id="javascript-package" />

## JavaScript / TypeScript package

Import a stylesheet from `@lumis-sh/themes/css/<theme>.css`.

```js
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:

```html
<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/`.

```elixir title="endpoint.ex"
plug Plug.Static,
  at: "/themes",
  from: {:lumis, "priv/static/css/"},
  only: ["catppuccin_frappe.css"]
```

```heex title="layout"
<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/*.css`
- `packages/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-*`.

```css
.l-comment {
  font-style: normal;
}
```

If the pre-built files do not fit, generate a stylesheet at runtime with the [CSS Builder](/themes/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`](/themes/css-builder#colors-without-layout) or use
`lumis themes build-css <theme> --no-layout`:

```css
@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: block` line plus the newline
  after it renders an extra blank row, and `code { display: grid }` makes some
  engines leave empty lines out of copied text.
- Add `box-sizing: border-box` when a line has horizontal padding or a border,
  so it still fits in `width: 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](/recipes/tailwind-css).

If your styles target `div.l-line`, follow the
[HTML line markup migration](/migrate/html-lines#custom-css).