CSS light-dark Function: Simplifying Dark Mode

The CSS light-dark() function modernizes theme management by allowing developers to declare both light and dark color values within a single property declaration. Traditionally, implementing dark mode required writing repetitive media queries or maintaining complex CSS custom variable swaps across multiple selectors. By pairing directly with the standard color-scheme property, light-dark() streamlines style sheets, reduces code complexity, and makes component-level theme overrides intuitive and maintainable.

The Limitations of Traditional Dark Mode Approaches

Historically, creating a dual-theme website required one of two patterns:

  1. Media Queries: Writing separate @media (prefers-color-scheme: dark) blocks that duplicate selectors to override color properties.
  2. Class-Based Variable Swapping: Defining design tokens as CSS custom properties on :root, then redefining those exact same variables under a .dark class or inside a media query.

While functional, both approaches fragment style declarations. To see what a specific component looks like in both themes, developers had to cross-reference multiple parts of a stylesheet or maintain long lists of abstract CSS custom properties.

How light-dark() Works

The light-dark() function takes two arguments, both of which must be valid color values:

color: light-dark(light-value, dark-value);

The browser evaluates the computed value of the element's color-scheme property to determine which color to apply. If color-scheme resolves to light, the first color is used. If it resolves to dark, the second color is rendered.

To enable this behavior globally, you must declare the supported schemes on the root element:

:root {
  color-scheme: light dark;
}

body {
  background-color: light-dark(#ffffff, #121212);
  color: light-dark(#222222, #e0e0e0);
}

If the user's operating system is set to dark mode, the browser automatically switches the active color-scheme to dark, which immediately resolves all light-dark() instances to their second parameter.

Granular, Component-Level Theming

A major advantage of light-dark() over traditional methods is local encapsulation. Because the function responds directly to the local color-scheme context, you can force a specific component into light or dark mode simply by altering its color-scheme property.

.card {
  background-color: light-dark(#f8f9fa, #1f1f1f);
  border: 1px solid light-dark(#e9ecef, #333333);
}

/* Force this specific element to always remain dark */
.card-featured {
  color-scheme: dark;
}

Any child elements inside .card-featured that use light-dark() will automatically render their dark variations without requiring specialized utility classes or overridden custom properties.

Integration with CSS Custom Properties

The light-dark() function works seamlessly with CSS variables. You can store your theme-reactive colors inside custom properties to maintain a single design token architecture:

:root {
  color-scheme: light dark;
  --surface: light-dark(#ffffff, #18181b);
  --text-primary: light-dark(#09090b, #fafafa);
  --border: light-dark(#e4e4e7, #27272a);
}

.button {
  background-color: var(--surface);
  color: var(--text-primary);
  border: 1px solid var(--border);
}

This pattern eliminates the need to declare duplicate :root blocks for different themes, consolidating token declarations into a single, cohesive system.

Browser Compatibility

The light-dark() function is part of the Baseline modern web features, supported across all major rendering engines including Chromium (Chrome/Edge 123+), Gecko (Firefox 120+), and WebKit (Safari 17.5+). Adopting it reduces stylesheet file sizes, prevents context-switching when editing components, and provides a native, resilient standard for modern web theming.