Dark mode and theming done right
Dark mode is a genuine accessibility and comfort feature for a large number of people, and it's also the single most common place where a good design system quietly falls apart. The reason is almost always the same: the second theme was added at the end, by inverting things, rather than designed alongside the first.
Start with tokens, not colours
The foundation of any theming system is naming colours by role rather than appearance. A variable called --surface-raised or --text-secondary can hold a different value in each theme and every component keeps working. A variable called --light-grey becomes a lie the moment the background turns dark.
Get the roles right and theming becomes a data change: one block of variables for light, one for dark, and no component ever knows which is active. Get them wrong and you'll be writing per-component overrides forever.
Design the dark palette properly
- Avoid pure black and pure white. White text on black creates halation — letters appear to glow and smear, particularly for readers with astigmatism. Use a very dark grey surface and an off-white text colour.
- Elevation is lightness, not shadow. Shadows barely register on a dark background. Signal that a card sits above the page by making its surface slightly lighter.
- Desaturate. Saturated brand colours that sing on white tend to vibrate painfully on dark. Most palettes need lighter, softer variants for dark mode.
- Re-check contrast in both themes. Passing contrast in light mode guarantees nothing about dark. Muted secondary text is the usual casualty.
- Don't forget the rest. Images with white backgrounds, embedded charts, code blocks, PDFs and email templates all need thought or they'll punch a bright hole in the page.
The switching mechanics
Three behaviours make a theme system feel considered. First, default to the system preference — the user already told their device what they want. Second, offer an explicit toggle with three states if you can: light, dark, and follow-system. Third, persist the choice so it survives a reload.
Then handle the one bug everyone ships at least once: the flash of wrong theme. If the saved preference is applied by your main JavaScript bundle, the browser has already painted a white page by the time it runs. The fix is a very small inline script in the document head that reads the stored preference and stamps it on the root element before anything renders.
If your dark mode was generated rather than designed, users can tell within a second.
Beyond dark mode
A token system that supports two themes usually supports more for free — high-contrast modes, per-customer branding in a white-label product, seasonal variants. That's the real return on the investment: you didn't build dark mode, you built theming, and dark mode was the first customer. Respect reduced-motion and reduced-transparency preferences with the same mechanism and you've covered most of what users ask for.
- Name colour tokens by role, so themes are a data swap rather than a rewrite.
- Design the dark palette deliberately — no pure black, no pure white, desaturated accents.
- Show elevation with lighter surfaces, not shadows.
- Default to the system preference, offer a toggle, and remember the choice.
- Apply the theme before first paint to avoid the white flash.
Frequently asked questions
Is dark mode just inverting the colours?
No. Inverting produces harsh, unreadable interfaces — pure white text on pure black causes halation, and inverted brand colours often break entirely. Dark mode is a second designed palette where every colour is chosen for a dark surface, not calculated from the light one.
Should the site follow the system setting or a toggle?
Both. Default to the operating system preference so the site matches what the user already chose, and offer a toggle for people who want something different on your site specifically. Remember their choice so it persists between visits.
What is the flash of wrong theme and how do I stop it?
It's the moment a page paints in light mode before the script applies the saved dark preference. Fix it by setting the theme on the root element in a tiny inline script that runs before the page renders, rather than after your main JavaScript loads.
ZIVARA builds design systems that scale across themes, brands and platforms. Let's talk. Related: design systems that scale and building accessible UI components.