TaskBuild a card that uses light-dark() for its surface + text colors. :root sets color-scheme light dark. .card has padding 20px, background light-dark(white, oklch(0.22 0 0)), color light-dark(oklch(0.20 0 0), oklch(0.95 0 0)), border 1px solid light-dark(oklch(0.90 0 0), oklch(0.30 0 0)), border-radius 12px.
light-dark(): one color, two themes, zero @media
100 XP8 min
Theory
A 2024 CSS feature you already wanted
You've been writing this for years:
:root { --bg: white; }
@media (prefers-color-scheme: dark) {
:root { --bg: oklch(0.18 0 0); }
}light-dark() collapses both into one expression:
:root {
color-scheme: light dark;
--bg: light-dark(white, oklch(0.18 0 0));
}The browser picks the first argument when the user is in light mode, the second in dark. The color-scheme declaration tells the browser "this element supports both" β required for light-dark() to know what "light" and "dark" actually mean.
Why this is better than @media
- Less duplication β each token defined once, both values inline.
- Local overrides β a single component can swap its dark color without redefining the whole root variable cascade.
- Forced-scheme APIs β
color-scheme: only darkat a subtree level still works;light-dark()reads the cascaded value.
Browser support
Chromium 123, Safari 17.5, Firefox 120 β all shipped 2024. Use a fallback for older Chromium:
:root { --bg: white; }
@supports (color: light-dark(white, black)) {
:root { --bg: light-dark(white, oklch(0.18 0 0)); }
}
@media (prefers-color-scheme: dark) {
:root:not(:has(*)) { --bg: oklch(0.18 0 0); } /* legacy path */
}For greenfield 2026 code, just use light-dark() and accept that pre-2024 Chromium gets the first arg always.
π
Sign up to start coding
Theory is open to everyone. The interactive editor, live preview, and check are unlocked with a 7-day free trial β card required, cancel anytime.
Sign up β free trial βFirst 10 lessons in each track are free. No card needed for those.