Why inversion doesn't work
A naive dark mode inverts the light palette: white backgrounds become near-black, dark text becomes near-white, accents are kept roughly the same. This approach fails because light and dark modes have fundamentally asymmetric perceptual requirements. Light mode's core challenge is legibility on high-reflectance surfaces. Dark mode's core challenge is managing contrast so text is readable but not harsh, colors are vivid but not aggressive, and spatial hierarchy is communicated through lightness rather than shadow. An inverted light palette doesn't address any of these — it produces a UI that looks like someone dimmed the lights rather than designed for the dark.
Building a dark surface system with lightness elevation
Depth in dark mode UIs is expressed through lightness levels: darker surfaces recede, lighter surfaces advance. The standard system has 4-5 distinct lightness levels, each representing a different elevation: base background (L:10-12%), slightly elevated surfaces like sidebars (L:14-16%), card surfaces (L:17-20%), overlay surfaces like modals and drawers (L:22-26%), and tooltip/popover surfaces (L:26-30%). The lightness differences are subtle — only 3-5 points between levels — but perceptually clear because they're the primary depth cue available in dark mode. This system is most legible when surface borders (at L:25-30%) and subtle separators (L:20-22%) are also controlled precisely. Android's Material Design and Apple's Human Interface Guidelines both specify elevation-based surface systems along these principles.
Saturation re-calibration for dark surfaces
Every brand color in a design system needs individual saturation re-calibration for dark mode. Blues and cyans are naturally more visually intense on dark backgrounds and often need 20-30% saturation reduction. Reds and oranges maintain their perceived intensity better and typically need only 10-15% reduction. Greens can become over-bright in dark mode and often benefit from a slight hue shift toward teal (3-5° cooler) in addition to saturation reduction. Yellows are the most problematic in dark mode: full-saturation yellow on a dark background is extremely aggressive and nearly impossible to soften without losing its yellow identity. Dark mode warning colors (semantic yellow) are almost always completely redesigned — typically shifting toward amber or warm orange — rather than desaturated from the light mode yellow.
Semantic colors across modes
Semantic colors — success (green), warning (yellow-amber), error (red), info (blue) — must maintain their semantic legibility in both modes while being individually calibrated for each. The most important constraint is that all four semantic colors must be distinguishable from each other in both modes for users with color vision deficiency. Design each semantic color with hue and lightness differentiation, not just hue. In dark mode: success green should shift toward a higher-lightness, slightly teal-adjusted green (lighter and cooler than the light-mode version); warning should shift to amber-orange; error red can remain similar but slightly lighter; info blue should be light enough to be distinguishable from the dark background without neon-level brightness. Test the full semantic palette in the ColorArchive WCAG Audit tool for each mode.
Implementation with CSS custom properties
The recommended implementation uses CSS custom properties (CSS variables) for all color values, with dark mode as a parallel variable set. Structure: define all light mode colors in :root { } and override with dark mode values in @media (prefers-color-scheme: dark) :root { } for automatic OS-level detection. For a manual toggle: apply data-color-scheme='dark' to the <html> element and target [data-color-scheme='dark'] in your CSS, alongside the media query. Map your surface system to semantic token names: --surface-base, --surface-raised, --surface-overlay, --text-primary, --text-secondary, --text-disabled, --border-default, --border-strong. Each token maps to different actual color values in light and dark contexts. This approach — used by Radix UI, shadcn/ui, and most modern design systems — allows components to be authored once while responding correctly to both color schemes.