A Small Thing About the CSS light-dark() Function
The era of duplicating variables across massive media query blocks is ending thanks to a native way to declare twin values for any property.
I remember staring at a 400-line CSS file a few years back, scrolling frantically between the top of the :root and a massive media query at the bottom just to keep track of my grays. It felt like I was writing the same stylesheet twice, just with the lights turned off and my eyes squinting at the screen.
The "traditional" way of handling dark mode has always been a bit of a chore. You define your variables, then you wrap their overrides in a @media (prefers-color-scheme: dark) block. It works, but it’s repetitive. It splits the logic for a single color into two different places in your codebase.
The CSS light-dark() function finally fixes this by letting us define both values in one go.
The Boilerplate We’re Leaving Behind
Normally, your CSS looks something like this:
:root {
--bg-color: #ffffff;
--text-color: #1a1a1a;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a1a;
--text-color: #f0f0f0;
}
}
body {
background-color: var(--bg-color);
color: var(--text-color);
}It’s fine, but as your design system grows, that media query block becomes a graveyard of duplicated variable names. If you rename --bg-color to --surface-main, you have to remember to change it in two places. It's just enough friction to be annoying.
Enter light-dark()
The light-dark() function is a native CSS feature that takes two arguments. The first is for light mode, and the second is for dark mode.
Here is that same logic, compressed:
:root {
color-scheme: light dark;
--bg-color: light-dark(#ffffff, #1a1a1a);
--text-color: light-dark(#1a1a1a, #f0f0f0);
}
body {
background-color: var(--bg-color);
color: var(--text-color);
}That’s it. No media query blocks scattered across the file. The "twin" values live together, making it immediately obvious what a color turns into when the user toggles their OS settings.
The One Rule: You Must Set color-scheme
There is a catch, and it’s the reason most people think the function is "broken" when they first try it.
For light-dark() to work, the browser needs to know that the element supports both modes. If you don't tell the browser that your document supports dark mode, it will always default to the "light" value, regardless of the user's system settings.
You fix this by setting the color-scheme property on the :root (or on specific elements):
:root {
/* This is the magic switch */
color-scheme: light dark;
}If you only want a specific section of your page to react to light/dark settings—maybe a sidebar or a card—you can set the color-scheme on just that container.
Why This Actually Matters
Aside from saving lines of code, this function makes scoped components much easier to manage.
If you're building a component library and you want a button that has a specific border color in dark mode, you don't have to write a global media query. You can keep the logic inside the component's class:
.card {
background: light-dark(#fff, #222);
border: 1px solid light-dark(#ddd, #444);
padding: 1rem;
border-radius: 8px;
}It keeps the "state" of the element’s appearance localized.
The Current State of Support
As of right now, browser support is actually quite good—Chrome, Firefox, and Safari all support it in their recent versions. However, if you have to support older browsers (I'm sorry), you'll still need a fallback.
One way to handle it is to provide a standard variable definition before the light-dark() version:
.brand-box {
background: #007bff; /* Fallback */
background: light-dark(#007bff, #004a99);
}It’s a small addition to the CSS spec, but it’s one of those quality-of-life improvements that makes the language feel more modern and less like a series of hacks layered on top of each other. It’s cleaner, it’s faster to write, and it keeps your brain from melting while you're trying to sync up your theme variables at 2:00 AM.


