One property. Two modes. No JavaScript.
light-dark() became baseline in July 2024. One CSS function, one property, two values — the browser picks based on prefers-color-scheme. The JavaScript theme toggle becomes optional.
light-dark() became Baseline in July 2024. One function, two color values — the browser picks based on whether the system preference is light or dark.
:root {
color-scheme: light dark;
}
body {
background: light-dark(#f5f2eb, #1a1814);
color: light-dark(#1a1814, #f5f2eb);
}
No [data-theme="dark"] attribute selector. No @media (prefers-color-scheme: dark) block. No class swap. No localStorage. No JavaScript.
Browser support: Baseline July 2024. Chrome 123+, Firefox 120+, Safari 17.5+. MDN reference. The
color-scheme: light darkdeclaration on:rootis required — without it,light-dark()resolves to its light value in all contexts.
A full design token system with light-dark()
:root {
color-scheme: light dark;
--bg: light-dark(#f5f2eb, #1a1814);
--bg-elevated: light-dark(#ffffff, #252118);
--bg-muted: light-dark(#e8e3d8, #2a261e);
--text: light-dark(#1a1814, #e8e3d8);
--text-muted: light-dark(#5a554c, #9a9388);
--text-faint: light-dark(#6a6560, #706a60);
--border: light-dark(#e8e3d8, #2e2a20);
--accent: light-dark(#c4522a, #e06a3a);
}
One color-scheme declaration on :root. The browser reads the system preference and resolves every light-dark() call accordingly.
What the JavaScript toggle buys you
Eliminating JavaScript isn’t always an improvement. It’s a choice with consequences.
A JavaScript theme toggle does something light-dark() alone cannot: it lets the user override the system preference. light-dark() responds to prefers-color-scheme. If the user’s OS is set to light mode but they prefer a dark site, light-dark() shows them light.
- Pure
light-dark(): respects OS preference. No override. No JS. Zero friction for users who configure their OS correctly. - JS toggle +
[data-theme]selectors: respects OS preference and allows override. Requires JavaScript and persistence.
The answer depends on your users. A developer tool: the audience manages their OS themes — pure light-dark() is appropriate. A consumer product: many users set their OS to light and expect an in-app dark mode setting.
Hybrid: light-dark() as fallback, JS toggle as override
:root {
color-scheme: light dark;
/* light-dark() provides the default */
--bg: light-dark(#f5f2eb, #1a1814);
}
/* JS toggle overrides via data-theme attribute */
[data-theme="dark"] {
--bg: #1a1814;
color-scheme: dark;
}
[data-theme="light"] {
--bg: #f5f2eb;
color-scheme: light;
}
Without any data-theme attribute, the system preference applies via light-dark(). When the user toggles, JS sets data-theme and the attribute selectors override. Best of both.
The color-scheme property on individual elements
/* This element always renders in dark mode, regardless of system preference */
.code-block {
color-scheme: dark;
background: #1e1e1e;
color: #e0e0e0;
}
Setting color-scheme on an element also tells browser-rendered UI inside it (scrollbars, form controls, system elements) which mode to use. The property propagates to UA-rendered content in a way that background-color doesn’t.
What breaks when you go pure light-dark()
If you convert the entire token system to light-dark() and remove [data-theme] selectors, specificity needs auditing. [data-theme="dark"] has specificity [0, 1, 0] — one attribute selector — which beats :root’s [0, 0, 1]. If your JS toggle overwrites at the :root level via custom property re-declaration, the specificity model needs adjustment.
The simpler path: keep the JS toggle, add light-dark() as the default values in your token declarations and let the JS override only where needed. Graceful degradation (the site works without JS, with correct OS-matched colors) plus user override capability (the toggle works as before).
One property. Two modes. The rest is preference.