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:
| What | Minimum ratio | Level |
|---|---|---|
| Body text | 4.5:1 | AA |
| Large text — 18 pt, or 14 pt bold | 3:1 | AA |
| UI component boundaries and graphics | 3:1 | AA |
| Body text | 7:1 | AAA |
| Large text | 4.5:1 | AAA |
"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:
| Colour | Hex | Ratio on #0a0811 | Verdict |
|---|---|---|---|
| Cream, headings | #fffdf8 | 19.6:1 | Passes AAA |
| Ink, body text | #eceaf6 | 16.7:1 | Passes AAA |
| Light lavender | #c4b9fb | 11.1:1 | Passes AAA |
| Lavender, links | #ab9ff2 | 8.5:1 | Passes AAA |
| Grey, secondary text | #9b95ae | 6.9:1 | Passes AA, not AAA |
| Deep lavender | #8b7ce0 | 5.7:1 | Passes AA |
| Dim grey, metadata | #6d6684 | 3.7:1 | Large text only |
| Hairline border | #241f36 | 1.3:1 | Decorative 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:
| Combination | Ratio | Body text at 4.5:1 |
|---|---|---|
White on lavender #ab9ff2 | 2.3:1 | Fails badly |
| Near-black on the same lavender | 8.2:1 | Passes |
| White on a saturated green | 2.2:1 | Fails badly |
| Black on the same green | 9.5:1 | Passes |
| White on a saturated red | 4.1:1 | Fails, 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:
- Alpha text over a gradient or an image has a different ratio at every point. It must pass at the lightest and the darkest.
- Stacked translucent surfaces — a sheet over a card over the background — shift the effective background under the text, so the same token can pass in one context and fail in another.
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
- Background off-black, not
#000000. - Body text at 4.5:1 or better against the surface it actually sits on.
- Coloured buttons and badges: dark label, not white.
- Anything below 4.5:1 must be genuinely optional to read.
- Focus rings and control boundaries at 3:1; dividers exempt.
- Never signal state with colour alone — pair it with a glyph or a word.
- Check the palette on a phone at minimum brightness, outdoors. It is the condition your design will most often be read in, and no automated tool simulates 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 walletKeep reading
- How to show a crypto portfolio in a pitch deckA portfolio screen in a deck is a product illustration, not evidence. Here is how to make it legible at slide scale, and…
- Crypto wallet UI design: what the good ones have in commonSix patterns repeat across every wallet worth copying. Here they are, with the measurements and the thresholds behind them.
- Figma mockup vs real screenshot for a product demoBoth produce an image at the same pixel size. They fail in opposite ways, and the failure mode is what should decide.