Open my wallet

Designing for dark mode: contrast rules that hold up

The thresholds that apply, what changes when the background is near-black, and the measured ratios of a real dark palette.

Published:

Dark mode uses the same WCAG thresholds as everything else — 4.5:1 for body text, 3:1 for large text and for the parts of a control that identify it — but a near-black background makes them easy to pass and easy to overshoot, so the real work is deciding which elements are allowed maximum contrast.

The thresholds do not change

There is no separate dark-mode standard. The same numbers apply:

WhatMinimum ratioLevel
Body text4.5:1AA
Large text — 18 pt, or 14 pt bold3:1AA
UI component boundaries and graphics3:1AA
Body text7:1AAA
Large text4.5:1AAA

"Large" in CSS terms is roughly 24 px, or 18.66 px when bold. The ratio itself is (L1 + 0.05) / (L2 + 0.05), where L is relative luminance — which is why the scale runs from 1:1 to a ceiling of 21:1, the ratio of pure white to pure black.

Why dark mode flatters the numbers

Against a near-black background almost any legible foreground passes. A mid grey that would fail on white sails through on black. This is genuinely easier — and it is where dark themes go wrong, because passing is not the goal. If every element passes comfortably, nothing is dimmer than anything else, and the interface loses its hierarchy while scoring perfectly on the audit.

In dark mode, contrast is the hierarchy. Primary text near the top of the range, secondary text comfortably above 4.5:1, and non-essential metadata down near the floor — but only where losing it costs the user nothing.

Measured: a real dark palette

Ratios computed with the WCAG formula against #0a0811, the background of this site. These are calculated, not estimated:

ColourHexRatio on #0a0811Verdict
Cream, headings#fffdf819.6:1Passes AAA
Ink, body text#eceaf616.7:1Passes AAA
Light lavender#c4b9fb11.1:1Passes AAA
Lavender, links#ab9ff28.5:1Passes AAA
Grey, secondary text#9b95ae6.9:1Passes AA, not AAA
Deep lavender#8b7ce05.7:1Passes AA
Dim grey, metadata#6d66843.7:1Large text only
Hairline border#241f361.3:1Decorative only

Two of those rows are worth reading as warnings rather than results. The dim grey at 3.7:1 is below the body-text threshold: it is fine for a decorative timestamp and wrong for anything a reader needs, and if you use a colour like it for small text you are making a trade, not passing a test. The hairline at 1.3:1 is nowhere near 3:1, which is correct for a divider — 3:1 applies to boundaries that identify a control, not to every line on the screen. Get that distinction wrong and you end up with an interface full of glowing borders in the name of accessibility.

We ran these numbers on our own palette while building My Larp Wallet, and adjusted two colours because of them. The calculation takes a minute and is worth doing on any palette before it is committed to a design system.

Do not put pure white on pure black

21:1 is the maximum available ratio and it is not the target. Bright text on a true black background produces halation — letterforms appear to bleed outward — and the effect is worse for readers with astigmatism, which is a large share of the population. Long-form reading at maximum contrast is tiring in a way that a contrast checker cannot see.

The convention that emerged is an off-black surface and an off-white foreground. Material Design's dark theme baseline is #121212, which still gives 18.7:1 with white — an enormous margin, with the harshest edge taken off. Our own background is a very dark violet rather than a grey, for the same reason.

The trap: coloured backgrounds

This is where most dark themes actually fail, and the failure is on the bright elements rather than the dark ones. Every ratio below is measured:

CombinationRatioBody text at 4.5:1
White on lavender #ab9ff22.3:1Fails badly
Near-black on the same lavender8.2:1Passes
White on a saturated green2.2:1Fails badly
Black on the same green9.5:1Passes
White on a saturated red4.1:1Fails, narrowly

The pattern: an accent colour bright enough to glow on a dark background is too light to carry white text. Primary buttons and status badges therefore take dark labels, which looks wrong for about a day and then looks correct permanently. The red row is the dangerous one — 4.1:1 is close enough to pass an eyeball check and it still fails. It is the same conclusion we reached measuring gain and loss badges in the anatomy of a wallet portfolio screen, from the opposite direction.

Elevation has to come from the surface

In a light theme, depth comes from shadow. In a dark theme a shadow lands on a background that is already almost black, so it does nothing — which is why dark interfaces that were designed light first look uniformly flat.

The convention that works is the opposite: the higher the surface, the lighter it is. A card sits a step above the background, a sheet a step above the card, a menu above that. Material Design formalises this as an overlay of white at increasing opacity per elevation level. Two constraints follow. Each step must be visible — an increment too small to perceive is wasted complexity — and the steps have to stop before the top surface competes with the text on it. Three or four levels is usually the whole budget.

Practical consequence for text tokens: your secondary grey has to clear 4.5:1 against the lightest surface it can land on, not the background. That is the ratio to record in the design system.

Opacity is not a colour

Dimming text with opacity or an alpha channel does not change what the checker needs to see. The ratio depends on the composited result: white at 60 % over a dark background is a specific grey, and that grey is what must be measured. Two consequences:

Where a value is fixed, resolve it to a hex colour once and use that. Alpha is for effects that genuinely need to composite, not for making a palette smaller.

A checklist that catches most of it

Compose the screen instead of describing it

My Larp Wallet lays out a wallet screen in the browser and exports it at 1179 × 2556 px, the native resolution of an iPhone 14 Pro. What comes out is a mockup, and it says so. One plan, $19/month.

Open my wallet

Keep reading