CSS light-dark(): Theme Colors Without the Gotchas
Theme-aware colors in pure CSS — but only if color-scheme is set, inherited correctly, and browsers actually support it.
The light-dark() function lets you declare colors for both themes in a single property:
color: light-dark(#333, #eee). No media queries, no duplicated custom properties.
Sounds perfect — until you hit the gotchas around color-scheme inheritance,
browser support fallbacks, and the surprising ways it can silently fail.
The Required Prerequisite: color-scheme
Here's the first trap: light-dark() does nothing without
color-scheme being set. If you write this:
/* ❌ light-dark() returns light value unconditionally */
body {
color: light-dark(#333, #fff);
}
...you'll always get #333. The browser defaults to light scheme when
color-scheme isn't declared. You must explicitly opt in:
/* ✅ Now responds to system preference */
:root {
color-scheme: light dark;
}
body {
color: light-dark(#333, #fff);
}
The light dark value tells the browser to support both schemes and
respect the user's OS preference via prefers-color-scheme.
color-scheme Inheritance: The Silent Override
color-scheme is an inherited property. This enables powerful patterns...
and subtle bugs. Consider:
:root {
color-scheme: light dark;
}
.card {
/* Forces light scheme for this subtree */
color-scheme: light;
background: light-dark(white, #1a1a1a);
/* Always white — dark value never used! */
}
Every light-dark() inside .card (and its children) now
ignores dark mode. This is intentional if you're creating a "forced light" section.
It's a bug if you forgot that color-scheme cascades.
Debugging Tip
In Chrome DevTools, inspect an element and look at the Computed tab for
color-scheme. If it shows light (not light dark),
something in the ancestor chain is forcing light scheme.
Fallback Strategies for Older Browsers
light-dark() shipped in Chrome 123 (March 2024), Safari 17.4, Firefox 120.
As of 2026, browser support is strong (~95%) but you still need fallbacks for older devices:
:root {
color-scheme: light dark;
/* Fallback for browsers without light-dark() */
--text-color: #333;
--bg-color: white;
}
@media (prefers-color-scheme: dark) {
:root {
--text-color: #eee;
--bg-color: #1a1a1a;
}
}
body {
/* Modern browsers use light-dark(), others use fallback vars */
color: var(--text-color);
color: light-dark(#333, #eee);
background: var(--bg-color);
background: light-dark(white, #1a1a1a);
}
The second declaration overwrites the first in supporting browsers.
Unsupporting browsers ignore the invalid light-dark() and keep the fallback.
JS Detection: @supports Won't Work
You might try @supports (color: light-dark(red, blue)) — but this
can give false positives in some browser versions. For reliable detection:
// Reliable feature detection
const supportsLightDark = CSS.supports('color', 'light-dark(red, blue)');
if (!supportsLightDark) {
document.documentElement.classList.add('no-light-dark');
}System Colors: The Free Dark Mode
Before reaching for light-dark(), know that setting color-scheme: light dark
already gives you automatic dark mode for native HTML elements and system color keywords:
:root {
color-scheme: light dark;
}
body {
/* Automatically flips with theme! */
background: Canvas;
color: CanvasText;
}
a {
color: LinkText;
}
input, button {
/* Form elements auto-adapt too */
}
Canvas, CanvasText, LinkText, ButtonFace —
these system colors
automatically adjust to the current color scheme. You only need light-dark()
for custom colors.
Combining with color-mix()
light-dark() works with any color value, including color-mix():
:root {
color-scheme: light dark;
--brand: #4a90d9;
}
.card {
background: light-dark(
color-mix(in oklab, Canvas 95%, var(--brand)),
color-mix(in oklab, Canvas 85%, var(--brand))
);
}This creates a subtle branded tint that's lighter in light mode and more prominent in dark mode.
Edge Case: Forced Colors Mode
When Windows High Contrast mode (forced-colors) is active, light-dark()
is ignored and system colors are used instead. Test with:
@media (forced-colors: active) {
.button {
/* Your light-dark() values won't apply here */
/* Use system colors or forced-colors-adjust */
border: 2px solid ButtonText;
}
}The Meta Tag Alternative
Instead of CSS, you can declare color scheme support in HTML:
<meta name="color-scheme" content="light dark">
This applies before CSS loads, preventing flash of wrong colors on page load.
The CSS color-scheme property can still override per-element.
When to Use (and When Not To)
- Use light-dark(): For inline color values where you want theme-awareness without variables
- Use custom properties: When colors are reused across many rules, or you need JS access
- Use @media prefers-color-scheme: When you need different layouts/images per theme, not just colors
- Use system colors: For standard UI elements — they're already theme-aware
light-dark() is a welcome addition, but it's not a silver bullet.
The color-scheme dependency and inheritance behavior mean you need
to understand the full system — not just the function itself.
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement