Manual Style Theming Dark mode

Dark mode

elements man style/theming/dark-mode Read as markdown

Dark mode is built into the default theme. You don't build a second stylesheet or add dark: variants to your markup. The semantic tokens flip, and everything built from them follows.

How it works

themes/default.css defines the light semantic tokens on :root, then remaps the same tokens for dark mode. The ramps don't move; the semantic pointers do. --bg goes from white to --neutral-950, --ink from --neutral-900 to --neutral-50, and so on for every surface, ink, rule, accent, and status token.

Because components never check the mode (they only read semantic tokens), anything you build from tokens is dark-ready for free. A card that uses var(--bg) and var(--ink) is correct in both modes with no extra rules.

OS preference by default

Out of the box the app follows the operating system's preference through @media (prefers-color-scheme: dark). A user whose OS is in dark mode sees your app in dark mode with no configuration.

Forcing a mode with data-theme

To let users override the OS preference, set a data-theme attribute on <html>:

  • data-theme="dark" forces dark mode.
  • data-theme="light" forces light mode (it also suppresses the OS dark preference).

The attribute always wins over the OS setting. Wire it to a user toggle (whatever JavaScript your app uses to flip the attribute) and persist the choice. The dark token set is defined twice in themes/default.css (once in the media query, once under the attribute) and the two blocks are kept identical.

<!-- OS preference -->
<html>

<!-- force dark regardless of OS -->
<html data-theme="dark">

<!-- force light regardless of OS -->
<html data-theme="light">

Writing dark-ready CSS

Consume semantic tokens and you never write a dark-mode rule:

/* Correct in both modes automatically */
.panel {
  background: var(--bg-panel);
  color: var(--ink);
  border: 1px solid var(--rule);
}

Reaching past the semantic tokens to a raw ramp step (--neutral-50) opts that value out of the flip, so it stays light in dark mode. Stick to --bg*, --ink*, --rule*, --accent*, and the status tokens.

Exception: email

The email baseline is exempt. Mail clients can't reliably resolve CSS variables, so email.css ships raw values and is light-only. See email.

color-scheme

The theme also sets the CSS color-scheme property (light on :root, dark in the dark blocks), so native form controls, scrollbars, and the browser chrome match the active mode.