Dark Mode Done Right: Inverting a Palette Without Wrecking the Brand
A naive invert turns brand reds into teals and breaks every gradient. Here is how to design a dark mode that feels intentional, not auto-generated.
Most dark modes go wrong in the same way: someone runs filter: invert(1) in Figma, screenshots the result, and ships. The brand color hue rotates, photographs become negatives, and shadows go from "depth" to "missing image". Here's the disciplined alternative.
Rule 1: invert lightness, keep hue
Working in OKLCH makes this trivial. For every neutral, swap the L value: 0.05 -> 0.95, 0.20 -> 0.80, 0.50 stays 0.50. Hue and chroma stay put. The neutrals "feel" mirrored without color shifts.
Rule 2: dim the saturated colors
Saturated colors look noticeably more aggressive on dark backgrounds. Drop the chroma by ~15-25% on every brand and semantic color in dark mode. The eye experiences the same intensity. Skipping this step is why so many dark modes feel "neon".
Rule 3: surfaces lighten as they raise
In light mode, raised surfaces get darker shadows. In dark mode, they get lighter backgrounds. A modal in dark mode is a paler shade of the page background, not a black box with a glow. This is how Material 3 and macOS both handle elevation.
--surface: oklch(0.16 0.01 270); /* page */
--surface-raised: oklch(0.20 0.01 270); /* card */
--surface-popped: oklch(0.24 0.01 270); /* modal/menu */
Rule 4: text never goes pure white
Pure white (#fff) on dark backgrounds creates harsh halation. Use ~95% L instead. Body text at oklch(0.92 0 0) is comfortable; pure white feels like staring at a phone in bed.
Rule 5: re-audit contrast
WCAG ratios don't translate from light to dark automatically. White-on-#1f1f2e is not the same ratio as black-on-#fefefe. Run every text-on-surface pair through the matrix view of Contrast Checker for the dark theme separately.
Rule 6: imagery and shadows are different problems
- Photos: don't invert them. Add a slight
filter: brightness(0.9)if they feel too punchy on dark backgrounds. - Shadows: blacks are invisible. Use a brighter blur (low-opacity white) or a colored glow (the brand color at 10-15% opacity).
- Borders: instead of darker borders, use a 1px lighter hairline at ~12% opacity.
Generate both modes from one source
Our Design System tool generates a 12-step OKLCH ramp from your brand color and emits both light and dark token sets at once. Drop the output into your CSS variables and the rules above are baked in.