Theming with CSS custom properties, the hybrid way

A no-image article — pure reading. Auto-follow the OS, but let an explicit choice win. A small pattern that scales to any number of named themes.

Most theming setups force a choice: follow the system, or expose a manual toggle. You can have both, with a predictable precedence rule and almost no JavaScript.

The precedence rule

Treat an explicit data-theme as the source of truth; fall back to the OS only when it's absent:

:root { /* canonical dark */ }
@media (prefers-color-scheme: light){
  :root:not([data-theme="dark"]):not([data-theme="dark-knight"]){ /* light */ }
}
:root[data-theme="desert-dunes"]{ /* light, forced */ }
Note Set color-scheme per theme so native controls match.

Naming the schemes

Names beat "light/dark" for a product voice — ours are Dark Knight and Désert Dunes, each with an alias so the plain values still work.

The toggle should change one attribute and persist one string. Everything else is CSS.

Takeaways

  • Explicit choice wins over the OS
  • One attribute drives every component
  • Add a theme = add a token block + a name
G
Grace H.

Engineer on Qazana Strata. Writes about CSS architecture and theming.