Dark mode
elements man style/theming/dark-mode Read as markdownDark 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.