Azalea
Azalea

Theming

Three states, not two. “Follow the OS” is the setting most people actually want, and a two-way toggle silently takes it away the first time it is clicked.

Try it

This toggle is the real component; it changes this page.

Light · Dark · System

How it resolves

An explicit choice always beats the OS. Nothing is written to the DOM for “system”, so the media query takes over again.

:root                                  /* light — the default */
@media (prefers-color-scheme: dark)
  :root:not([data-theme="light"])      /* OS dark, unless overridden */
:root[data-theme="dark"], [data-theme="dark"]    /* explicit dark, wins */
:root[data-theme="light"], [data-theme="light"]  /* explicit light, wins */

Pinning a subtree

Because the selectors match any element and not only :root, a container can pin its contents to a theme. That is how the previews on this site show both at once — and it is the only honest way to check contrast, since a component can look fine in the theme you are in and be unreadable in the other.

<div data-theme="dark">
  {/* renders dark whatever the page is doing */}
</div>

light

Same markup

No conditional styling.

RUNNING

dark

Same markup

No conditional styling.

RUNNING

Rules

  • Render <ThemeScript /> in <head>. Without it the page paints light and corrects on hydration.
  • Every localStorage access is wrapped in try/catch. A private window or blocked site data throws on read, and a theme toggle must never be the thing that takes a page down.
  • Use dark: variants sparingly — for genuine one-offs like swapping a logo asset. If you are reaching for it often, the token you want is missing.