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.
Table of Contents
- 1. The core idea: a value pair instead of two separate rule blocks
- 2. color-scheme: the prerequisite for light-dark() to work at all
- 3. Comparison to prefers-color-scheme media queries
- 4. Manual dark mode toggle by overriding color-scheme
- 5. light-dark() with custom properties for design token systems
- 6. Browser support and safeguarding with @supports
- 7. When switching from media queries to light-dark() pays off
- 8. Practical example: a complete color scheme for a component
- 9. Accessibility: check contrast for both modes separately
- 10. Summary
- 11. FAQ
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.