Dark Mode Done Right: Inverting a Palette Without Wrecking the Brand

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.