The Problem with Traditional Dark Mode
Implementing dark mode traditionally required duplicate CSS rules wrapped in @media (prefers-color-scheme: dark):
:root {
--bg: #ffffff;
--text: #111111;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a2e;
--text: #e0e0e0;
}
}
This works but creates maintenance overhead, especially when managing multiple color properties across large stylesheets. The light-dark() CSS function eliminates this duplication entirely.
The light-dark() Function
The light-dark() function accepts two color values and automatically returns the one matching the user’s current color scheme:
color: light-dark(black, white);
/* Returns black in light mode, white in dark mode */
The first argument is the light-mode color, the second is the dark-mode color. The function respects the user’s system preference or the color-scheme property.
Setting Up color-scheme
For light-dark() to work, you must declare which color schemes the page supports:
<meta name="color-scheme" content="light dark" />
Or in CSS:
:root {
color-scheme: light dark;
}
This tells the browser the page supports both schemes and enables the light-dark() function.
Simplifying Theme Variables
With light-dark(), the previous example becomes a single declaration:
:root {
color-scheme: light dark;
--bg: light-dark(#ffffff, #1a1a2e);
--text: light-dark(#111111, #e0e0e0);
--primary: light-dark(#2563eb, #60a5fa);
--border: light-dark(#d1d5db, #374151);
--shadow: light-dark(rgba(0,0,0,0.1), rgba(0,0,0,0.4));
}
No media queries needed. The browser handles the switching automatically.
Comparison: Media Query vs light-dark()
| Aspect | @media prefers-color-scheme | light-dark() |
|---|---|---|
| Lines of code | ~10 per variable pair | 1 per variable |
| Maintenance | Dual blocks to update | Single line |
| JavaScript toggle | Needs manual class toggling | Compatible with color-scheme override |
| Browser support | Universal | Chrome 123+, Firefox 128+, Safari 17.5+ |
| Granularity | Per-block | Per-value |
Toggling Programmatically
For manual theme toggles (not just system preference), change the color-scheme on the root element:
// Toggle between light and dark manually
document.documentElement.style.colorScheme =
document.documentElement.style.colorScheme === "dark"
? "light"
: "dark";
This works seamlessly with light-dark() because the function reads the current color-scheme value.
Combining with Custom Properties for Complex Themes
For full control, combine light-dark() with semantic custom properties:
:root {
color-scheme: light dark;
/* Surface colors */
--surface-primary: light-dark(#ffffff, #0f172a);
--surface-secondary: light-dark(#f8fafc, #1e293b);
/* Text colors */
--text-primary: light-dark(#0f172a, #f1f5f9);
--text-secondary: light-dark(#475569, #94a3b8);
/* Accent colors */
--accent: light-dark(#3b82f6, #818cf8);
--success: light-dark(#22c55e, #4ade80);
--danger: light-dark(#ef4444, #f87171);
}
Browser Support and Fallbacks
As of 2026, light-dark() is supported in Chrome 123+, Firefox 128+, and Safari 17.5+. For older browsers, provide fallback values:
.button {
/* Fallback for older browsers */
background: #2563eb;
/* Modern override */
background: light-dark(#2563eb, #1d4ed8);
}
Migration Strategy
- Add
color-scheme: light darkto:root - Replace
@mediablocks withlight-dark()function calls in custom properties - Test both themes using DevTools rendering tab
- Remove legacy
@mediablocks after verification - Add JavaScript toggle support for manual theme switching
Conclusion
CSS light-dark() represents a significant simplification for dark mode implementation. By eliminating duplicate code blocks and letting the browser handle theme switching natively, developers can maintain cleaner stylesheets with less cognitive overhead. It’s a rare example where a new CSS feature simultaneously reduces code size, improves maintainability, and enhances user experience.
