Every few months someone ships a dark theme by inverting their palette, and every few months it looks wrong in the same ways. Dark mode is not a color problem — it is a contrast problem.
It's not inversion
Pure inversion takes a page that read as calm and turns it into a page that glows. White text at full strength on near-black vibrates; hairlines that were subtle become invisible; borders that were invisible become loud. The relationships between colors matter more than the colors themselves.
Start from the text, not the background. Pick the body color first, then find the darkest background that still gives it comfortable contrast.
The gray trap
A muted gray that sits at 4.6:1 on cream can drop below 3:1 on charcoal without changing a single digit. Every gray in the palette needs its own dark twin — --muted is a pair of values, not one.
If a token has one value for both themes, it is probably wrong in at least one of them.
Elevation without shadows
Shadows are nearly invisible on dark backgrounds, so depth has to come from the surface itself: the closer a layer is, the lighter it gets. A step of two or three percent lightness is usually enough.
Accessible accents
Saturated brand colors that pass on white often fail on dark backgrounds — blue is the classic case. The fix is not desaturation, it is lightening: keep the hue, raise the lightness until the contrast comes back.
--accent-light: #2563EB; /* on #F4F2EC */
--accent-dark: #7AA2FF; /* on #161513 */
Check both against real body text sizes, not just large headlines.
A short checklist
- Body text at 4.5:1 or better in both themes.
- Every gray token has a dark twin.
- Depth from lightness steps, not shadows.
- Accents re-tuned, not reused.