7 min read

Dark mode is not a colour inversion

The cheap way to build a dark theme is to swap the background and text tokens and ship it. It produces something that is recognisably dark and quietly broken, and the breakage clusters in four predictable places.

Depth runs the other way

In a light interface, elevation is made with shadow. A card sits above the page because it casts one. Invert the background and the shadow disappears, because a dark shadow on a dark surface is nothing.

Dark interfaces build depth with lightness instead: surfaces get lighter as they come forward. That inverts the relationship between your tokens (a card that was lighter than the page in light mode is still lighter than the page in dark mode), so the whole scale has to be re-derived rather than flipped.

The practical consequence is that you cannot express the dark palette as a transformation of the light one. There are two palettes, and they have to be designed together.

Borders are where it fails first

Hairline borders are the most common casualty. A border at 1.3:1 against its background looks like a refined detail on a bright screen in a bright room. On a dim laptop at an angle it is invisible, and any control whose only boundary was that border has stopped being a control.

WCAG 1.4.11 asks for 3:1 on the visual boundary of a user interface component. That threshold is worth splitting your tokens over: one line colour for anything that bounds something interactive, held above 3:1 in both themes, and a separate decorative one for rules and dividers where the requirement genuinely does not apply.

Two tokens instead of one, but it removes a whole class of judgement call: a designer reaching for a border no longer has to work out which kind this is.

The accent stops working

Brand accents are chosen against white. Most of them are mid-tone saturated colours that pass comfortably on a light background and fail on a dark one, or the reverse.

Worse, the same accent rarely works as both a fill and a text colour. An orange that makes a good button fill on light paper is usually too light to be readable text on that same paper, and the white you were going to put on the button often does not pass either.

The fix is to stop treating the accent as one value. Separate the fill, the text colour, and the overprint that sits on the fill. On this site those are three tokens: the fill carries no text below 24px, the overprint on it is near-black, and small accent-coloured text uses a darker variant entirely. In the dark theme the fill and the text value converge, because against a dark background they can.

Focus rings need a theme too

A focus indicator built from one hard-coded colour will pass in one theme and quietly fail in the other. It has to be a token, and it has to be checked against every surface it can land on: inside cards, on top of the accent fill, and over any image.

The same applies to form error states. If invalid is communicated by a red border alone, you have failed 1.4.1 before anyone looks at what that red does on a dark surface. Give the state a border weight change and a written message, and colour becomes reinforcement rather than the signal.

Check it with a script, not an eye

All of this is arithmetic. Relative luminance and contrast ratio are about twenty lines of code, and every pair in a palette can be checked in a fraction of a second.

Doing that while the palette is being chosen is what makes it cheap. Discovering that your accent fails as body text after the site is built is not a fix, it is a redesign. By then the colour is in the logo, the buttons, the links, and the client's expectations.

The palette on this site was written as a table of pairs and run through a script before a single component existed. Two combinations failed on the first pass and were changed that afternoon, which cost nothing.

Working on this?

If this describes something you are building, send the specifics. The general version is above; the useful conversation is about your version.

Ask AMDV

Answers come from this site only

AMDV

Tell me what you are trying to build, or ask about scope, price, or timeline. I answer from this site's content and point you at the page that covers it.

Start with

Enter sends. Shift plus Enter adds a line. 900 characters maximum.