refract v0.1 · beta
Cookbook / Dark mode

Dark mode

Dark mode is a property mode, not a second theme. Author a dark value on the properties that change; the CSS adapter emits both an OS-preference @media block and a manual [data-theme] toggle from the same variable names — so it follows the system with zero JavaScript, and a one-line attribute forces it.

1 · Author the mode

Put modes: [{ mode: "dark", … }] on the properties that differ — usually a handful of surface / ink / border colours. Everything that references those tokens flips for free; you don't mode every property.

theme.ts
const raw = {
  colors: {
    surface: { base: "#ffffff", modes: [{ mode: "dark", base: "#0b0d12" }] },
    ink:     { base: "#0e1117", modes: [{ mode: "dark", base: "#e9ecf3" }] },
    recipes: { solid: { panel: { background: "surface", color: "ink" } } },
  },
};

2 · What the CSS adapter emits

Each moded property is redeclared under two selectors — the same --dt-… names, so one redefinition flips every downstream var(--…) through the cascade:

theme.css (emitted)
:root {
  --dt-colors-surface: rgb(255, 255, 255);
  --dt-colors-ink: rgb(14, 17, 23);
}
@media (prefers-color-scheme: dark) {   /* follows the OS — no JS */
  :root {
    --dt-colors-surface: rgb(11, 13, 18);
    --dt-colors-ink: rgb(233, 236, 243);
  }
}
:root[data-theme="dark"] {              /* manual toggle — wins over the OS block */
  --dt-colors-surface: rgb(11, 13, 18);
  --dt-colors-ink: rgb(233, 236, 243);
}
.dt-colors-solid-panel { background: var(--dt-colors-surface); color: var(--dt-colors-ink); }
dark and light are the first-class modes — they get the OS-preference @media block. A custom mode (say hc for high-contrast) has no OS signal, so it emits only the :root[data-theme="hc"] block — a manual toggle.

3 · Toggle at run-time

Do nothing and it follows the OS. To let the user override, set data-theme on the root element — its higher specificity wins over the media block. Remove it to fall back to the OS.

toggle.ts
const root = document.documentElement;

root.setAttribute("data-theme", "dark");   // force dark
root.setAttribute("data-theme", "light");  // force light
root.removeAttribute("data-theme");         // back to the OS preference
This is exactly how this site's own light/dark toggle works — set data-theme, or leave it off to follow your system.

modes vs override()

Both give you a dark UI; they solve different problems.

modesoverride()
whatA per-property variant baked into one stylesheet.A separate child Theme object (its own stylesheet).
toggleCSS cascade — flip data-theme, or follow the OS. No recompile, no JS needed for OS.Swap the stylesheet / re-read child.css in app state.
use whenDark is a variation of the same theme (the common case).You need a genuinely different theme — distinct presets, programmatic access, run-time-computed values.

They compose: an override() child still emits its own modes blocks. Reach for modes first for dark mode; reach for override() when you need a distinct theme object.

See RawTheme anatomy for the modes shape and Colors for authoring colour values. Modes work on any property subsystem, not just colours.
Technical documentation for @theme-registry/refract · all output is compiled client-side by the real library (CSS adapter). The chrome is theme-aware; the render panes carry each preset's own world. · MIT licensed.