light-dark(): The CSS Function for Color-Scheme-Dependent Values
AI generated
{ }
@
CSS · Color Scheme · Dark Mode
The light-dark() Function
Define color values directly based on the color scheme, no media query needed

With the light-dark() function, a light and a dark mode color value can be specified in a single declaration, instead of maintaining two separate prefers-color-scheme blocks. The browser automatically picks whichever of the two values applies based on the currently active color-scheme, both for system preference and for a manual toggle that overrides it.

11 min read light-dark() · color-scheme CSS Color Module Level 5

1. The core idea: a value pair instead of two separate rule blocks

The function light-dark(<light>, <dark>) takes exactly two color values: the first for light mode, the second for dark mode. Which of the two values actually applies is decided by the browser based on the currently effective color-scheme, which can be set either through the color-scheme CSS property on an ancestor element or through the system preference prefers-color-scheme.

That merges two previously separate concepts into a single declaration: the actual color definition and the condition for when each color applies. This considerably reduces repetition in stylesheets, because no individual property needs its own prefers-color-scheme: dark block anymore, the case distinction lives directly inside the value itself.


:root {
  color-scheme: light dark;
}

body {
  /* One value pair instead of two media query blocks */
  background: light-dark(#ffffff, #0f172a);
  color: light-dark(#1e293b, #e2e8f0);
}

2. color-scheme: the prerequisite for light-dark() to work at all

For light-dark() to work, the property color-scheme: light dark; must be set on the root element or an ancestor. This declaration tells the browser that the document supports both modes, and only then activates the automatic selection through light-dark(). Without this declaration, light-dark() always behaves as if only light mode were active, regardless of the user's system preference.

That coupling is not accidental: color-scheme controls not just light-dark(), but also native browser UI elements like form fields, scrollbars, and the address bar's system color on mobile devices. Anyone already setting color-scheme for a consistent native look gets light-dark() essentially for free as an added capability.

3. Comparison to prefers-color-scheme media queries

The classic approach to dark mode support consists of a media query block that redeclares every affected property inside @media (prefers-color-scheme: dark) { ... }. That works reliably, but on larger stylesheets it means the same selectors exist twice, once for the default case and once for dark mode, which makes maintenance and consistency checking harder the moment a color value changes in one block but gets forgotten in the other.

With light-dark(), the case distinction sits directly next to the property it applies to, making both states immediately visible when reading the code instead of scattered across two distant locations. That noticeably lowers the risk of forgetting a dark mode color, because there is no second block left to forget.


/* Classic approach: two separate blocks */
.card {
  background: #ffffff;
  border-color: #e2e8f0;
}

@media (prefers-color-scheme: dark) {
  .card {
    background: #1e293b;
    border-color: #334155;
  }
}

/* With light-dark(): a single block */
.card {
  background: light-dark(#ffffff, #1e293b);
  border-color: light-dark(#e2e8f0, #334155);
}

4. Manual dark mode toggle by overriding color-scheme

A common requirement is a toggle in the user interface that lets users manually force dark mode regardless of the system preference. Because light-dark() strictly follows the effective color-scheme value, it is enough to set that property through JavaScript on the root element, for example light or dark instead of light dark, to override the automatic system detection.

The advantage over the classic media query approach is that this manual toggle needs no extra CSS class and no second set of selectors. A single line of JavaScript that sets color-scheme on the html element is enough for every light-dark() call in the whole stylesheet to immediately reflect the desired mode.


<script type="text/plain">
// User manually selects dark mode, independent of the system
document.documentElement.style.colorScheme = 'dark';

// Back to following system preference
document.documentElement.style.colorScheme = 'light dark';
</script>

5. light-dark() with custom properties for design token systems

In larger projects with their own design token system, light-dark() can be applied directly where custom properties are defined, instead of repeating it at every single usage site. That centralizes the color scheme logic in one single place in the stylesheet and makes every further use of the custom property automatically color-scheme-aware, without the calling code needing to know about it.

This centralization pays off especially when a token like --surface-primary is used in many different places across a project. If the concrete dark mode color changes later, a single change to the token definition is enough, instead of searching dozens of usage sites throughout the project and adjusting each individually.


:root {
  color-scheme: light dark;
  --surface-primary: light-dark(#ffffff, #0f172a);
  --surface-secondary: light-dark(#f8fafc, #1e293b);
  --text-primary: light-dark(#0f172a, #f1f5f9);
  --border-subtle: light-dark(#e2e8f0, #334155);
}

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

6. Browser support and safeguarding with @supports

light-dark() is supported by all current versions of Chrome, Edge, Firefox and Safari, after the function landed in Safari, Chrome and Firefox nearly simultaneously in 2023. For most projects that no longer need to serve very old browser versions, the function is therefore usable in production without major restrictions.

For projects with stricter compatibility requirements, support can be specifically tested with @supports (color: light-dark(#fff, #000)). The modern declaration sits inside the rule, while a classic, media-query-based fallback outside the rule still works correctly in older browsers.

7. When switching from media queries to light-dark() pays off

The switch pays off especially in projects with many individual color declarations that were previously spread across numerous scattered prefers-color-scheme blocks. Here, light-dark() not only reduces the line count in the stylesheet, it also lowers the risk that a dark mode value simply gets forgotten during a change, because both values sit inseparably on the same line.

A full switch is less sensible when dark mode changes more than pure color values, for example different image sources, font weights, or layout properties. For such structural differences, a classic prefers-color-scheme block remains the right tool, while light-dark() is applied specifically to pure color decisions, often combined within the same project.

8. Practical example: a complete color scheme for a component

Using a card component with a border, shadow, and an accent color as an example shows the pattern in full: every visual property that differs between light and dark gets its own light-dark() value pair, while structural properties like padding or border radius stay unchanged, since they are independent of the color scheme anyway.

It matters not to forget shadow colors either: a shadow that looks subtly gray in light mode can become entirely invisible in dark mode if it is not also adjusted through light-dark(). A darker but more intense shadow value in dark mode ensures the component's sense of depth is preserved in both modes.


.product-card {
  background: light-dark(#ffffff, #1e293b);
  border: 1px solid light-dark(#e2e8f0, #334155);
  box-shadow: 0 4px 12px light-dark(rgba(15,23,42,0.08), rgba(0,0,0,0.4));
  color: light-dark(#0f172a, #f1f5f9);
  padding: 1.5rem;
  border-radius: 0.75rem;
}

.product-card .price {
  color: light-dark(#7c3aed, #c4b5fd);
}

9. Accessibility: check contrast for both modes separately

Because light-dark() puts both color pairs so compactly side by side, it is easy to assume a value pair is automatically contrast-safe for both modes just because it is syntactically grouped together. In reality, contrast ratios for light and dark mode need to be checked independently, since a gray tone that offers sufficient contrast against white in light mode can turn out clearly too weak against a dark background in dark mode.

In practice this means checking every light-dark() pair twice with a contrast checker, once for the light foreground and background combination, once for the dark variant. Tools that calculate WCAG contrast ratios should therefore be included in the review process for new light-dark() declarations, not applied as an afterthought once a finished screenshot exists.

Approach Lines of code per color Risk of forgotten dark mode values Suited for
@media (prefers-color-scheme: dark) 2 (two blocks) Higher, values spatially separated Structural differences, not just color
light-dark() per property 1 Lower, values on one line Individual, scattered color declarations
light-dark() in custom properties 1 (centralized) Lowest, one token per color Design token systems
Manual JS toggle via color-scheme 0 additional Unchanged from the chosen method User-controlled dark mode toggle

Mironsoft

Modern CSS, layout architecture and rendering performance

CSS that stays maintainable instead of breaking with every change?

We review existing stylesheets for specificity chaos and layout thrashing, then build a CSS architecture with cascade layers, custom properties and modern layout primitives that still makes sense after the tenth feature.

CSS Audit

Systematically uncovering specificity issues, cascade conflicts and unused selectors.

Architecture Refactoring

Introducing cascade layers, custom properties and design tokens cleanly.

Performance Tuning

Fixing layout thrashing, expensive selectors and rendering bottlenecks.

10. Summary

light-dark() in CSS: The Essentials at a Glance

Core idea

light-dark(light, dark) defines a color value pair in a single declaration, the browser picks based on color-scheme.

Prerequisite

Without color-scheme: light dark; on the root element, light-dark() stays permanently on the light value.

Advantage over media queries

Both states sit side by side in the code, which makes forgetting a dark mode value less likely.

Limits

For structural differences between modes, not just color, a classic prefers-color-scheme block is still needed.

11. FAQ: light-dark() in CSS: The Essentials at a Glance

1What does the light-dark() function do in CSS?
light-dark() takes two color values, one for light and one for dark mode, and the browser automatically picks the matching one based on the current color-scheme.
2What is required for light-dark() to work?
color-scheme: light dark; must be set on the root element or an ancestor. Without that declaration, light-dark() always stays on the light value.
3How does light-dark() differ from prefers-color-scheme media queries?
light-dark() defines both color values in a single line, while a media query requires two separate rule blocks. That lowers the risk of forgetting a dark mode value.
4Can I force dark mode manually, independent of the system?
Yes, by having a JavaScript toggle set the color-scheme property on the root element directly to light or dark instead of light dark, which overrides system preference.
5Which browsers support light-dark()?
All current versions of Chrome, Edge, Firefox and Safari, after the function landed in nearly all major browsers simultaneously in 2023.
6Should I use light-dark() in custom properties or directly in selectors?
For design token systems, defining it centrally in custom properties is recommended, so a later color change only needs to be maintained in one place.
7Does light-dark() replace prefers-color-scheme media queries entirely?
Not always. For structural differences between the modes, like different images or layouts, a classic media query block remains the right tool.
8Do I need to check contrast ratios for both modes separately?
Yes. A color value that offers sufficient contrast in light mode can be too weak in dark mode. Both combinations should be checked independently.
9Does color-scheme affect anything besides light-dark()?
Yes, color-scheme also controls native browser UI like form fields, scrollbars, and the address bar's system color on mobile devices.
10How do I safeguard light-dark() for older browsers?
With @supports (color: light-dark(#fff, #000)) you can specifically test whether the browser supports the function, with a classic media query fallback outside the rule for older browsers.