Making Color-Scheme-Aware CSS Simpler

CSS offers several ways to define colors: named colors, hex values, color functions tied to specific color spaces, and the generic color() function. The same color can be expressed in multiple formats. For example, cornflowerblue can also be written as #6495ED, hsl(218.54deg 79.19% 66.08%), or color(display-p3 0.43 0.58 0.9).

Beyond these author-defined colors, CSS includes system colors as defined in CSS Color Module Level 4. These are colors defined by the browser and referenced via keywords. Notable examples are Canvas (representing the background of application content) and CanvasText (representing text in that content). These two are designed to pair together. Typically, CanvasText resolves to a near-black color and Canvas to a near-white color, though exact values depend on the browser. Chrome renders CanvasText as #121212, while Safari uses the slightly lighter #1e1e1e.

A significant feature of system colors is their ability to react to the computed color-scheme property. When the used scheme is dark, the values of CanvasText and Canvas flip accordingly. The color-scheme property itself supports three relevant settings:

  • light dark: element supports both modes; the choice depends on the prefers-color-scheme media condition.
  • light: element only supports light mode.
  • dark: element only supports dark mode.

The light-dark() Function

System colors were, until recently, the only way to react to the used color-scheme. CSS Color Module Level 5 introduces light-dark(), which exposes that same capability directly to authors. The function takes two arguments, both of which must be <color> values. The browser picks one based on the used color scheme:

  • If the scheme is light (or unknown), the computed value of the first argument is returned.
  • If the scheme is dark, the computed value of the second argument is returned.

The output of light-dark() is a <color>, meaning it can be used anywhere a color value is accepted, including properties like color and background-color, as well as inside functions such as linear-gradient(). The example below sets the background color to #333 in dark mode and #ccc in light mode.

:root {
  color-scheme: light dark;
}

body {
  background-color: light-dark(#ccc, #333);
}

For light-dark() to work, a color-scheme must be declared. Because the property inherits, it is typically set on :root, though it can also be applied to specific elements as needed.

Replacing Variable Overrides

A common pattern for supporting dark mode involves setting custom properties that hold color values, then overriding them within a prefers-color-scheme media query to supply dark-mode alternatives.

The light-dark() function simplifies this significantly. With color-scheme: light dark set on :root, the values automatically switch when the OS preference changes between light and dark — no media query necessary:

:root {
  color-scheme: light dark;
  --text-color: light-dark(#333, #ccc);
  --background-color: light-dark(#fafafa, #1e1e1e);
}

An added benefit is the ability to force a particular subtree into a specific mode. Setting color-scheme to either light or dark on a container will make all light-dark() calls resolve accordingly, regardless of the OS-level setting.