EdgeCases Logo
Mar 2026
CSS
Surface
6 min read

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.

css
light-dark
color-scheme
dark-mode
theming
colors

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

Related Insights

Explore related edge cases and patterns

CSS
Surface
CSS @scope: Native Style Scoping Without Shadow DOM
6 min
CSS
Surface
CSS text-wrap: balance and pretty
6 min
CSS
Surface
CSS contain: Render Isolation for Performance
6 min

Advertisement