The toggle isn't the hard part
Flipping a `dark` class on `<html>` and inverting a handful of colors takes an afternoon. Making dark mode hold up across a growing product — new components, third-party widgets, charts, data tables with status colors — takes a system.
The failure mode is always the same: someone hardcodes `#1f2937` instead of a token, it looks fine in isolation, and six months later it's the one panel that stays white in dark mode and nobody remembers why.
Building it as tokens, not overrides
Name colors by role, not value
`--surface`, `--text-muted`, `--border` — never `--gray-200`. A role-based name can be redefined per theme without touching a single component.
Redefine tokens, not components
Dark mode should only ever touch the token layer. If a component file needs a `dark:` override, a token is missing — that's the bug to fix, not the override to add.
Support three states, not two
Light, dark, and system — where system means "no explicit choice, follow the OS." Treating "no preference" as its own state avoids fighting `prefers-color-scheme` later.
Keep status colors semantic too
Success, warning and danger colors need their own light/dark pairs. A green that passes contrast on white paper can fail badly on near-black backgrounds.
Set the theme before first paint
Read the stored preference in an inline script in `<head>`, before any stylesheet renders, so the page never flashes the wrong theme on load.
Audit contrast per theme, not once
A token pair that passes WCAG AA in light mode isn't guaranteed to pass in dark mode. Check both directions, especially for muted text and disabled states.
Third-party components are the real test
Your own components are easy to theme correctly because you control every color. The test is a component library, chart package or embedded widget that ships its own default palette and doesn't know your tokens exist.
The fix is the same as everywhere else: map the library's theming API — CSS custom properties, a theme preset, a config object — to your own semantic tokens once, centrally. Never patch an individual instance's colors; the next instance of that same component will just break the same way again.
The takeaway
Dark mode that lasts is a token architecture decision made once, not a CSS override added per component. Name colors by role, redefine only the token layer per theme, and map every third-party library into that same system — and new screens will be dark-mode-correct by default, not by remembering to check.
Let's talk about your frontendRelated reading
Visual Design · Color
Color as Function, Not Decoration: Building a Palette Enterprise Software Can Trust
UX · Interaction
Micro-Interactions That Earn Their Keep (and the Ones That Don't)
Visual Design · Iconography
Icon Systems: Consistency, Sizing and When Text Beats an Icon Entirely
See dark mode applied in production on the FarmGate case study.