Dark Mode Design: The Complete Color Guide
Dark mode is not filter: invert(1). It's a second color system with its own physics: on dark grounds, light text halates, saturated colors glow, shadows disappear, and elevation must be rebuilt from lightness. Done well, it reduces glare in dim environments and saves OLED battery; done by inversion, it's eye strain with extra steps.
The five rules
1. Never pure black. #000 backgrounds make text shimmer and kill elevation. Start around #0E1016β#121212 (Material's classic), reserving pure black for OLED-optimized media UIs.
2. Elevation = lightness. Shadows barely read on dark. Raise surfaces by lightening: background #0E1016 β card #151823 β popover #1C2030. Consistent steps create instant depth grammar.
3. Desaturate accents. Your light-mode brand blue will vibrate on dark. Reduce chroma ~10β20% and raise lightness so it passes contrast: in OKLCH this is a two-number tweak of the same hue.
4. Soften the text, keep the contrast. Pure white on dark glares; #E6E8F0-ish primaries at 15:1+ read comfortably. Secondary text must still clear 4.5:1 β dark mode fails are usually here.
5. Re-test everything. Contrast pairs, charts, focus rings, illustrations, and images (add subtle borders or dim with filter: brightness(.9) on large white images).
| Token | Light | Dark |
|---|---|---|
| --bg | #F7F8FB | #0E1016 |
| --surface | #FFFFFF | #151823 |
| --border | #E4E7EF | #262B3D |
| --text | #14161F | #EEF0F6 |
| --text-2 | #5B6172 | #9AA1B5 |
| --brand | #5B4BC4 | #8B7CF0 |
Implementation that scales
One semantic token layer (--surface, --text-2β¦) with two value sets, switched by data-theme and defaulting to prefers-color-scheme. Never reference raw palette colors in components. Generate a starter pair with our UI theme generator β it emits light and dark sets from one seed.