one @theme, many palettes — the cascade trick
A single @theme block defines your tokens once and emits
them to :root. Every utility it spawns reads the token through
a variable: .bg-accent { background-color: var(--color-accent) }.
That indirection is the superpower — override the
variable under a scoped selector and the whole subtree re-skins, with
zero utility recompilation. One palette becomes three (default / ocean /
forest) by toggling data-theme; dark mode becomes class-driven
by toggling .dark.
| strategy | trigger | what changes | recompile? |
|---|---|---|---|
| data-theme palette | set data-theme="ocean" on a container |
--color-accent / --color-surface for that subtree |
❌ no — pure CSS cascade |
| class dark mode | add .dark class on an ancestor |
tokens under .dark + every dark: variant |
❌ no — @custom-variant dark is pre-compiled |
| media dark mode (default) | OS prefers-color-scheme: dark |
only dark: variant utilities |
❌ no |
1 · brand palette switcher — data-theme re-skins the subtree
Click a preset. The stage below sets data-theme, which
overrides --color-accent / --color-surface for
its descendants. The swatches use bg-accent / bg-surface
— the same compiled utilities in every theme; only the variable
values cascade. The readout prints the live getComputedStyle
values as proof.
accent token
class = bg-accent
surface token
class = bg-surface
composed card
text-accent on bg-accent
border + bg + text all token-driven
theme: default
--color-accent: —
bg-accent → —
2 · class-based dark mode — @custom-variant dark
By default dark: follows the OS prefers-color-scheme
media query. Override the dark variant with
@custom-variant dark (&:where(.dark, .dark *)) and now
dark: utilities fire whenever a .dark class sits
anywhere up the tree — fully script-toggleable, no media query. Toggle
below and watch the dark: utilities (and the
.dark token override) apply.
Zero Gravity Pen
The same dark: utilities compiled once — they activate only
when .dark is an ancestor. No media query involved.
Also: .dark overrides --color-surface (token
scoping works for class themes too) — surface card below:
.dark override
.dark class: absent (light)
3 · the scoped cascade — which selector wins, for whom
Custom properties cascade like any other CSS value: an element inherits the
value from its nearest ancestor that declares it. :root
(from @theme) is the global default; a closer
[data-theme] or .dark shadows it for that subtree only.
| selector | scope | effect on descendants |
|---|---|---|
:root (via @theme) |
entire document | sets the global default for every --color-* token |
[data-theme="ocean"] |
that subtree only | shadows --color-accent/surface; siblings outside are untouched |
[data-theme="forest"] |
that subtree only | a second, independent palette — can coexist with ocean elsewhere on the page |
.dark |
that subtree only | shadows tokens AND triggers dark: variants (paired with @custom-variant) |
[data-theme="ocean"].dark |
intersection | both apply — later / more specific declaration wins per property |
intent → pattern
| intent | pattern | why |
|---|---|---|
| define global tokens | @theme { --color-accent: oklch(0.7 0.15 250); } |
emits to :root + generates bg-accent reading var(--color-accent) |
| brand palette per area | [data-theme="ocean"] { --color-accent: oklch(0.72 0.14 215); } |
same utility, cascaded value — no recompile, no dark: spam |
| switch at runtime | stage.dataset.theme = "ocean" |
one attribute flip re-skins the subtree; CSS engine handles the rest |
| class-based dark mode | @custom-variant dark (&:where(.dark, .dark *)); |
dark: now keys off .dark class, not the OS media query |
| data-attr dark mode | @custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *)); |
drive dark mode with data-theme="dark" instead of a class |
| dark token override | .dark { --color-surface: oklch(0.16 0.01 250); } |
re-scope a token under dark — every bg-surface updates automatically |
| multiple palettes coexist | two containers, different data-theme |
ocean sidebar + forest hero on the same page, same compiled CSS |