Open the settings of almost any modern app and you'll find the same three buttons: Light, Dark, System. Lea Verou argued in her August 2026 piece on dark mode toggles that the third one solves a problem almost nobody has - it's the data model leaking into the UI, not a decision users came looking for.
I agree, and I'll add an engineering reason of my own: two states aren't just simpler to read, they're harder to get wrong in code. The toggles below are real, not screenshots - play with them first.
All four combinations
Simulated - pick any tile. The real toggle is further down the page, backed by your actual OS setting and localStorage.
A shape you've seen before
This exact Light / Dark / System segment shows up in Tailwind's own docs, Ant Design, and Radix Themes.
A single icon button that flips on click - VitePress and Material Design both ship this shape.
Bluesky and Google Calendar bury the same three options inside a settings screen instead of a toolbar toggle.
The Tri-State Toggle Is Implementation-Driven UI
A theme resolves to exactly one of two values on screen - light or dark; there's no third rendering. The three-state control exists because the value can come from three different sources - an explicit choice or a fallback to the OS - not because the page can look three different ways. Users don't open a toggle to plan ahead; they open it because the screen is too bright right now. A third option for a problem nobody has yet optimizes for the model, not the moment.
CSS agrees with the two-value model too: light-dark() takes exactly two colors - there's no third argument for a system fallback.
Live, not simulated
:root {
color-scheme: light dark;
}
body {
background: light-dark(#fff, #111);
color: light-dark(#111, #fff);
}Two arguments, no more - the function is binary by construction, not by convention.
color-scheme is what actually decides: set it to light or dark on any element and every light-dark() value inside inherits that choice - no media query, no JavaScript computing colors.
color-scheme: light;This window's colors are set entirely by light-dark() - clicking only changes color-scheme; the browser repaints the rest.
Try it
Button
Segments
Switch
Simulate OS
Changes the window chrome below, and the site preview too whenever you're following System - never your real OS.
Your Site
This card renders in a real light or dark theme - flip any of the first three controls above.
- OS preference
- -
- Your override
- -
- Resolved theme
- -
Your override here is real - an actual localStorage entry backed by a live matchMedia listener. Reload the page and it persists exactly like Lea Verou describes.
Where Two-State Actually Gets Hard
None of this is free to build correctly - four places teams get it wrong:
- Resolve the theme in a synchronous inline script before the first paint, not in a post-mount effect - otherwise every hard refresh flashes the wrong theme for a frame.
- Only touch localStorage in response to a click. Clearing an override because it now matches the system silently drags the user along on the next OS-level change they never asked to follow.
- You don't have to store which shade the reader picked. A single "inverted" flag - compute the shade as the opposite of the current OS preference - needs one bit instead of a value. The catch: that flag is defined relative to the OS, so if the OS theme changes while it's set, the resolved theme silently flips with it, which is exactly what the bullet above warns against. Whether that's a bug or correct depends on what the reader meant by "inverted," and that's genuinely still open.
- Without cross-tab sync, flipping the toggle in one tab leaves every other open tab stale. The native fix is the storage event - it fires on every other same-origin tab (never the one that wrote the change) whenever localStorage changes, so listening for it and re-deriving the theme is enough; no polling, no BroadcastChannel needed unless you also have to coordinate across origins.
When Three States Still Earn Their Keep
None of this makes tri-state wrong everywhere - compare the Dropdown with the Button above and the difference is obvious: naming System outright fits a settings panel, where the reader already arrived to configure something ahead of time. A toggle next to the content they're reading is a different context, and there two states are enough.
Lea's own closing line is the one worth keeping: respect user effort. Don't hand someone a third option for a problem they don't have yet.