Lesson 17 of 30
Theming a value for dark mode has always meant writing it once, then writing an override for it again inside a media query or theme selector. light-dark() lets you declare both values in one line.
10 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
What does light-dark(#fff, #111) let you avoid writing?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
The traditional way to theme a CSS value looks like this: declare the light value normally, then declare a second, separate override for the dark value inside a @media (prefers-color-scheme: dark) block (or a [data-theme="dark"] selector, for sites with a manual toggle). Every themed value needs this two-declaration pattern, repeated for every property that changes between themes.
light-dark() collapses that into one declaration: color: light-dark(#fff, #111); says "use #fff in light mode, #111 in dark mode" in a single line, with no separate override block needed for that value.
There's one precondition: light-dark() only knows which value to pick because it reads the color-scheme property. Somewhere in scope — typically color-scheme: light dark; on :root — the page has to declare that it supports both themes. Without that declaration, light-dark() doesn't know which branch to take. The color-scheme property itself isn't new — it already exists specifically to tell the browser (and things like form controls and scrollbars) which theme(s) a page supports; light-dark() is just a function that reads it and switches on it.
The old two-declaration pattern:
.card {
border-color: #d1d5db;
}
@media (prefers-color-scheme: dark) {
.card {
border-color: #374151;
}
}
The same result with light-dark():
:root {
color-scheme: light dark;
}
.card {
border-color: light-dark(#d1d5db, #374151);
}
One declaration, no media query, and it composes cleanly with custom properties too: border-color: light-dark(var(--gray-300), var(--gray-700)); is a completely normal thing to write.
This maps directly onto a problem this codebase already deals with. Praxis's own app/globals.css already declares color-scheme for the page and maintains separate light and dark values for its design tokens across dark-mode media queries and a [data-theme] override — the same underlying problem light-dark() targets, just currently solved with the two-declaration pattern rather than the newer single-function one. That's not a criticism of the existing setup; it's the same tool this lesson is describing, just via the older mechanism.
light-dark() silently does nothing useful without color-scheme declared. If color-scheme: light dark (or only light / only dark) isn't set somewhere in scope, the function has no signal to switch on and the result is unreliable — this is the single most common reason it "doesn't work" in a first attempt.[data-theme="dark"] button) still works with light-dark() — as long as toggling the theme also updates color-scheme accordingly (directly or via the same selector that sets the toggle), since that's what the function actually reads."How would you theme a component for dark mode without duplicating every color declaration?" tests whether a candidate knows the modern single-declaration option exists, and — just as importantly — whether they know its limits well enough not to reach for it when the light/dark difference is more than a value swap.