Skip to main content
Contact
Design Systems

Dark mode is a token problem, not a colour problem

Teams usually implement dark mode twice: once by inverting colours, and again properly, after discovering that half the surfaces stopped having edges.

  • Jonas Wilder
3 min read

The first attempt at dark mode is nearly always an inversion. Take the light palette, flip the luminance, ship it. It looks approximately right in a screenshot and falls apart the moment somebody uses it.

Why inversion fails

A light theme is built on a specific relationship between surfaces: the page is the darker plane and a card sits above it, lighter, catching the light. Invert that mechanically and the card is now darker than the page — which is a legitimate look, but it is not the one the components were designed for, and every shadow in the system is now pointing the wrong way.

The deeper problem is that the two ramps are not symmetrical. Light themes tend to run from white down through a narrow band of greys. Dark themes run from near-black up through a much wider one. A pair of surfaces that reads as a clear step in one direction can flatten to nothing in the other.

The rule that fixes it

Stop thinking in colours and start thinking in roles. A surface is not “grey 100”; it is “the plane a card sits on”. Once the role is named, each theme answers it independently:

:root {
  --background: var(--color-base-50);
  --card: var(--color-white);
}

.dark {
  --background: var(--color-gray-950);
  --card: var(--color-gray-900);
}

Note that these are not inversions of each other, and they do not need to be. Each theme picks the value that plays the role correctly on its own ramp.

The case that still needs care

Some surfaces genuinely do not have a parallel answer. A recessed panel — a block that should sit below the surrounding band — is a clear step down in dark and almost invisible in light, because light’s deep surface is a different token entirely.

Those get an explicit pair, written on the element so the two values cannot drift apart:

<div class="bg-muted dark:bg-background">…</div>

It looks like a special case because it is one. The discipline is to check every surface relationship against both ramps rather than assuming the one you designed in holds in the other.

Things that should not flip

Not everything is a token. An accent colour that is deliberately identical in both themes should be declared once, not repeated in both blocks — a value that does not flip has no business in a per-theme block. And an image belonging to somebody else’s brand is a fact rather than a decision; it ships as exported, in both themes.

Jonas Wilder

Design director. Interested in the boundary where a design system stops being a component library and starts being a set of decisions.

More from Jonas Wilder
All posts

Ready to get off the ground?

Tell us what you're building. We'll tell you how we'd approach it — no pitch deck required.

Book a call