# Light/Dark Mode (/recipes/light-dark)

The multi-themes formatter renders one HTML block with CSS variables for each theme. You control which theme is active with CSS.

## 1. CSS `light-dark()` function

`light-dark()` selects colors using the element's `color-scheme`, which is inherited from its parent. Lumis leaves that property unset so code follows the page's theme, including a manual light/dark toggle. If the page has not enabled a color scheme, the light colors are used.

To follow the OS preference throughout the page, set:

```css
html {
  color-scheme: light dark;
}
```

For a manual toggle, set `color-scheme: light` or `color-scheme: dark` on the page or the section containing the code:

```css
[data-theme="light"] {
  color-scheme: light;
}

[data-theme="dark"] {
  color-scheme: dark;
}
```

A page-wide rule is optional. To follow the OS preference for one code block, use the [block attributes below](#opt-a-block-into-os-switching). A local `color-scheme: light dark` makes that block follow the OS even when its parent selects only light or dark, so omit it when the block should follow the page's toggle.

This switches `color` and `background-color` on every runtime that supports HTML Multi-Themes output. `lumis4j` does not support this formatter, and the CLI generates HTML for a browser to render.

`light-dark()` is a [color function](https://drafts.csswg.org/css-color-5/#light-dark), so those two properties are all it covers. `font-weight`, `font-style` and `text-decoration` are handled by whether your two themes agree:

- **They agree** — the shared value is written as an ordinary declaration. It is right in both schemes, and there is nothing to switch. Most theme pairs are entirely in this case, which is why the CSS above is usually all you need.
- **They disagree** — the light theme italicises comments and the dark one does not, say. Nothing in CSS can switch a font style off the color scheme, so the formatter writes no declaration at all and emits `--lumis-light-font-style` and `--lumis-dark-font-style` instead. Add these rules to use them:

```css
.lumis span {
  font-style: var(--lumis-light-font-style);
  font-weight: var(--lumis-light-font-weight);
  text-decoration: var(--lumis-light-text-decoration);
}

@media (prefers-color-scheme: dark) {
  .lumis span {
    font-style: var(--lumis-dark-font-style);
    font-weight: var(--lumis-dark-font-weight);
    text-decoration: var(--lumis-dark-text-decoration);
  }
}
```

If your app sets `color-scheme` independently of the OS preference, replace this media query with selectors that follow the app's theme setting so these properties switch with the colors.

No `!important`, and you should not add one. A disputed property is deliberately left out of the style attribute, so these rules have nothing inline to outrank, and they stay in your cascade where a `print` or `forced-colors` rule of yours can still win. They are safe to apply to every span: on a token whose themes agreed, the inline declaration outranks them and the shared value stands; on a token neither theme styled, the variable was never set, so the declaration drops out and your own styling of the block is what renders.

The trade-off is that a disputed property renders in neither scheme until you add those rules. That is the same choice [Shiki](https://shiki.style/guide/dual-themes) makes, and it is the only one that keeps the generated HTML from dictating your cascade.

### Opt a block into OS switching

These examples add `color-scheme: light dark` to the block's `<pre>` through `pre_attrs` (`preAttrs` in JavaScript). It applies to the block and its scrollbar without changing form controls or scrollbars elsewhere on the page.

### JavaScript / TypeScript

```js
import {highlight} from '@lumis-sh/lumis'
import {htmlMultiThemes} from '@lumis-sh/lumis/formatters'
import javascript from '@lumis-sh/wasm-javascript'
import latte from '@lumis-sh/themes/catppuccin_latte'
import frappe from '@lumis-sh/themes/catppuccin_frappe'

const html = await highlight(
  'const x = 1',
  htmlMultiThemes({
    language: javascript,
    themes: {light: latte, dark: frappe},
    defaultTheme: 'light-dark()',
    preAttrs: {style: 'color-scheme: light dark;'},
  })
)
```

### Rust

```rust
use lumis::{
    highlight, languages::Language, themes,
    HtmlMultiThemesBuilder,
};
use std::collections::HashMap;

let mut theme_map = HashMap::new();
theme_map.insert(
    "light".to_string(),
    themes::get("catppuccin_latte").unwrap(),
);
theme_map.insert(
    "dark".to_string(),
    themes::get("catppuccin_frappe").unwrap(),
);

let html = highlight(
    "const x = 1",
    HtmlMultiThemesBuilder::new()
        .language(Language::JavaScript)
        .themes(theme_map)
        .default_theme("light-dark()")
        .pre_attrs(vec![("style".into(), "color-scheme: light dark;".into())])
        .build()
        .unwrap(),
);
```

### Elixir

```elixir
Lumis.highlight!(
  "const x = 1",
  formatter:
    {:html_multi_themes,
     language: "javascript",
     themes: [
       light: "catppuccin_latte",
       dark: "catppuccin_frappe"
     ],
     default_theme: "light-dark()",
     pre_attrs: [style: "color-scheme: light dark;"]}
)
```

### Java

HTML Multi-Themes is not currently supported in `lumis4j`.

### CLI

```bash
lumis highlight index.js \
  --formatter html-multi-themes \
  --themes light:catppuccin_latte \
  --themes dark:catppuccin_frappe \
  --default-theme "light-dark()" \
  --pre-attr 'style=color-scheme: light dark;'
```


### Inline code

With `structure: "inline"`, Lumis emits only token spans and ignores `pre_attrs` and `code_attrs`. The spans inherit `color-scheme` from the element you put them in. Leave that element's scheme unset to follow the page, or opt it into OS switching:

```html
<code style="color-scheme: light dark;">
  <!-- Insert the inline spans from Lumis here. -->
</code>
```

Setting the scheme does not add the theme's text or background color to your element. See [Keeping the theme's colors](/formatters#keeping-the-themes-colors) for the attribute helpers that supply them.

## 2. `@media (prefers-color-scheme)` with CSS variables

Use this when you need more control than `light-dark()` provides, or when targeting browsers that don't support it yet. The inline styles render the light theme by default, and a media query overrides with dark theme variables.

Required CSS for the rendered HTML output:

```css
@media (prefers-color-scheme: dark) {
  .lumis,
  .lumis span {
    color: var(--lumis-dark) !important;
    background-color: var(--lumis-dark-bg) !important;
    font-style: var(--lumis-dark-font-style) !important;
    font-weight: var(--lumis-dark-font-weight) !important;
    text-decoration: var(--lumis-dark-text-decoration) !important;
  }
}
```

For runtimes that support HTML Multi-Themes, `default_theme: "light"` makes the light theme's colors render as inline styles. The media query then swaps to the dark theme's CSS variables when the OS is in dark mode. `lumis4j` does not support this formatter, and the CLI only generates the HTML.

The `!important` is unavoidable here: a declaration in your stylesheet cannot outrank one in a `style` attribute without it. If you would rather keep control of the cascade, drop `default_theme` entirely. Every theme is then a variable, nothing is inline, and the same rules work without `!important` — at the cost of a block that renders unstyled until your CSS loads.

### JavaScript / TypeScript

```js
import {highlight} from '@lumis-sh/lumis'
import {htmlMultiThemes} from '@lumis-sh/lumis/formatters'
import javascript from '@lumis-sh/wasm-javascript'
import latte from '@lumis-sh/themes/catppuccin_latte'
import frappe from '@lumis-sh/themes/catppuccin_frappe'

const html = await highlight(
  code,
  htmlMultiThemes({
    language: javascript,
    themes: {light: latte, dark: frappe},
    defaultTheme: 'light',
  })
)
```

### Rust

```rust
use lumis::{
    highlight, languages::Language, themes,
    HtmlMultiThemesBuilder,
};
use std::collections::HashMap;

let mut theme_map = HashMap::new();
theme_map.insert(
    "light".to_string(),
    themes::get("catppuccin_latte").unwrap(),
);
theme_map.insert(
    "dark".to_string(),
    themes::get("catppuccin_frappe").unwrap(),
);

let html = highlight(
    code,
    HtmlMultiThemesBuilder::new()
        .language(Language::JavaScript)
        .themes(theme_map)
        .default_theme("light")
        .build()
        .unwrap(),
);
```

### Elixir

```elixir
Lumis.highlight!(
  code,
  formatter:
    {:html_multi_themes,
     language: "elixir",
     themes: [
       light: "catppuccin_latte",
       dark: "catppuccin_frappe"
     ],
     default_theme: "light"}
)
```

### Java

HTML Multi-Themes is not currently supported in `lumis4j`.

### CLI

```bash
lumis highlight index.js \
  --formatter html-multi-themes \
  --themes light:catppuccin_latte \
  --themes dark:catppuccin_frappe \
  --default-theme light
```


## 3. Manual switching with JavaScript

Use this when you want a toggle button or user preference that's independent of the OS setting. Add a class to the root element and target it in CSS.

Required CSS for the rendered HTML output:

```css
html.dark .lumis,
html.dark .lumis span {
  color: var(--lumis-dark) !important;
  background-color: var(--lumis-dark-bg) !important;
  font-style: var(--lumis-dark-font-style) !important;
  font-weight: var(--lumis-dark-font-weight) !important;
  text-decoration: var(--lumis-dark-text-decoration) !important;
}
```

Toggle JavaScript:

```js
function setTheme(theme) {
  if (theme === 'dark') {
    document.documentElement.classList.add('dark')
  } else {
    document.documentElement.classList.remove('dark')
  }
}
```

This pattern works anywhere you render the generated HTML. In Phoenix LiveView, you'd wire the button to a `phx-click` event that toggles a class via a JS command. `lumis4j` does not support HTML Multi-Themes, and the CLI only generates the HTML for another renderer to display.

### JavaScript / TypeScript

```js
import {highlight} from '@lumis-sh/lumis'
import {htmlMultiThemes} from '@lumis-sh/lumis/formatters'
import javascript from '@lumis-sh/wasm-javascript'
import latte from '@lumis-sh/themes/catppuccin_latte'
import frappe from '@lumis-sh/themes/catppuccin_frappe'

const html = await highlight(
  code,
  htmlMultiThemes({
    language: javascript,
    themes: {light: latte, dark: frappe},
    defaultTheme: 'light',
  })
)
```

### Rust

```rust
use lumis::{
    highlight, languages::Language, themes,
    HtmlMultiThemesBuilder,
};
use std::collections::HashMap;

let mut theme_map = HashMap::new();
theme_map.insert(
    "light".to_string(),
    themes::get("catppuccin_latte").unwrap(),
);
theme_map.insert(
    "dark".to_string(),
    themes::get("catppuccin_frappe").unwrap(),
);

let html = highlight(
    code,
    HtmlMultiThemesBuilder::new()
        .language(Language::JavaScript)
        .themes(theme_map)
        .default_theme("light")
        .build()
        .unwrap(),
);
```

### Elixir

```elixir
Lumis.highlight!(
  code,
  formatter:
    {:html_multi_themes,
     language: "elixir",
     themes: [
       light: "catppuccin_latte",
       dark: "catppuccin_frappe"
     ],
     default_theme: "light"}
)
```

### Java

HTML Multi-Themes is not currently supported in `lumis4j`.

### CLI

```bash
lumis highlight index.js \
  --formatter html-multi-themes \
  --themes light:catppuccin_latte \
  --themes dark:catppuccin_frappe \
  --default-theme light
```


## Which approach to use

| Approach | When to use |
| --- | --- |
| `light-dark()` | Follows the element's `color-scheme`: OS preference with `light dark`, or a page-selected `light` or `dark`. Extra CSS handles any font properties the themes disagree on. |
| `@media prefers-color-scheme` | OS-driven but you need the CSS override pattern (wider browser support). |
| Manual switching | User-controlled toggle independent of OS setting. |