light-dark()
It gets rid of the duplicate token block, but only if you remember the one declaration everybody forgets.
The usual shape of a theme is a block of custom properties, then a second block overriding every one of them. light-dark() folds the two blocks into one.
:root {
color-scheme: light dark;
--paper: light-dark(#fdfcfb, #12241c);
--ink: light-dark(#1b1d1b, #f4f1ee);
}
The first value applies in light mode and the second in dark. Both live on the same line and I think that’s the actual benefit. A token and its counterpart can’t get out of sync when they’re eight characters apart.
None of this works without color-scheme. Leave it out and the function resolves to its light value and nothing else happens. There’s no warning anywhere. That’s the whole debugging story for this one. So I’ll say it twice: if light-dark() seems to be ignoring dark mode, you haven’t set color-scheme.
color-scheme is worth setting for its own sake anyway. It’s what tells the browser to render form controls, scrollbars and the canvas in their dark variants. A site that themes everything itself and forgets this ends up with dark cards and a bright white select.
It works with an explicit theme toggle too, which I had wrong for a while. I assumed a toggle meant going back to a second block keyed to a data-theme on the root. But the toggle only has to set color-scheme there.
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }
Every light-dark() on the page then follows the root’s scheme instead of the OS. The tokens stay in their one block. This site still has a toggle and two blocks, which I realize is a slightly funny position to be recommending the other approach from.