Dark interfaces that stay readable
A dark interface fails in the same ways a light one does, plus a few of its own. Greys that looked fine on a bright monitor in an office disappear on a phone at night, a heavy glow turns small text into mush, and the pure black and white that feels bold in a mockup can feel harsh after a paragraph. Most of the fixes are measurable, and every ratio below was computed with the formula in the WCAG 2.2 text.
Start from the numbers
WCAG 2.2 defines contrast as a ratio between the relative luminance of two colours, from 1:1 (identical) to 21:1 (black and white). The thresholds do not change for dark themes:
| What | Minimum | Success criterion |
|---|---|---|
| Normal text | 4.5:1 | 1.4.3 Contrast (Minimum), AA |
| Large-scale text: 18 point, or 14 point bold | 3:1 | 1.4.3 |
| User interface components and states, meaningful graphics, measured against adjacent colours | 3:1 | 1.4.11 Non-text Contrast, AA |
| Enhanced text contrast | 7:1 (4.5:1 for large-scale text) | 1.4.6, AAA |
| Inactive controls, pure decoration, logotypes | no requirement | exceptions in 1.4.3 and 1.4.11 |
WCAG states large text in points. At the usual conversion of 1 point to 1.333 CSS pixels that is 24 px, or about 18.7 px in bold. That pixel figure is a conversion, not wording from the standard.
The ratio is (L1 + 0.05) / (L2 + 0.05), where L1 is the relative luminance of the lighter colour and L2 that of the darker. Relative luminance is 0.2126 R + 0.7152 G + 0.0722 B, where each channel is first divided by 255 and then linearised: values at or below 0.04045 are divided by 12.92, and larger values become ((value + 0.055) / 1.055) raised to the power 2.4. The 0.04045 threshold is the current definition in WCAG 2.2; the glossary notes that before May 2021 it was 0.03928, and that the change has no practical effect. For 8-bit colours the two thresholds give identical results, which a quick loop over all 256 values confirms.
Three details catch people out. First, the ratio is computed between the final rendered colours, so a semi-transparent text colour has to be resolved against the actual background first. Second, 1.4.11 asks for 3:1 only for the visual information needed to identify a component and its states. The Understanding document says a control with visible content, such as a text label, does not need a border, but where the boundary is the only way to see that an input exists, it must reach 3:1 against the adjacent colours. A decorative frame around a section has no requirement. Third, 1.4.3 applies to placeholder text as well as to ordinary text, according to the Understanding document, and it has no placeholder exception.
Why pure black and pure white are worth avoiding
The maximum, 21:1, is not the goal. WCAG sets minimums only and says nothing against a very high ratio. The case against #000 with #fff is about comfort, not compliance: some readers report that bright text on a pure black ground looks glaring or seems to bloom at its edges, and astigmatism is often mentioned as a reason. No W3C or other standards document cited here establishes that effect, so treat it as a common design observation, not a measured rule. A tinted near-black costs nothing in compliance and removes the question.
There is also a practical reason that needs no study: you cannot draw depth on a ground that is already black.
- Ground: a very dark colour with a hint of hue, such as a navy or neutral near-black, instead of
#000. A raised surface can then be lighter than the ground, and a shadow still has something to darken. - Primary text: an off-white tinted to the ground's hue. It should still land far above 4.5:1. On the navy ground used below,
#f2f5ffmeasures 17.38:1, against 19.28:1 on#000000, so almost nothing is given up. - Weight: light text on dark can look bolder than the same weight in dark on light, so check body copy at its real size and consider one step lighter. This is a rendering observation to verify on your own screens.
One more advisory from the Understanding document for 1.4.3: predominantly long-wavelength colours against a dark ground, which is to say red on black, are a problem for people with protanopia, and W3C lists avoiding that pairing as an advisory technique. A saturated red error message on near-black deserves a lighter, less saturated red and an icon or word as well.
A worked contrast table
Here is a fictional dark palette on a navy ground of #0b1020 (relative luminance 0.00546), with each level measured against it:
| Role | Colour | Ratio on #0b1020 | Use for | Verdict |
|---|---|---|---|---|
| Primary text | #f2f5ff | 17.38:1 | Body copy, headings | Pass AA and AAA |
| Secondary text | #b4bdd6 | 10.09:1 | Captions, long-form secondary copy | Pass AA and AAA |
| Tertiary text | #8d97b3 | 6.50:1 | Metadata, labels | Pass AA, misses the 7:1 AAA mark |
| Subdued text | #6b7594 | 4.14:1 | Looks "dim" | Fails normal text; large text or non-text only |
| Faint | #5a637f | 3.18:1 | Placeholder-ish | Large text and icons only |
| Faintest | #4a5370 | 2.49:1 | Tempting for hints | Fails everything |
| Accent link | #4f7cff | 5.10:1 | Link text | Pass AA |
| Accent dim | #3d6bf0 | 4.11:1 | Hover backgrounds, outlines | Fails normal text; fine for a 3:1 outline |
Two lessons come out of that table. A comfortable dim level for body-adjacent text sits around 6:1 instead of 4.5:1, because you want a margin for different screens and brightness settings. And there is a cliff: each step down looks only a little dimmer to the eye, yet the table crosses a pass line between rows three and four. Define the text levels as tokens and write each measured ratio in a comment. Never pick a grey by eye.
For comparison, white text on a neutral #121212 ground measures 18.73:1, #e0e0e0 on the same ground 14.19:1, and #9e9e9e 6.99:1, a hair under the 7:1 AAA line. A neutral near-black and a tinted navy both work. What matters is that text levels are chosen from measured values.
Surfaces, depth and borders
On light themes depth comes from shadows. On a dark theme shadows are nearly invisible against the ground, so depth comes from lightness steps: each raised layer is slightly lighter than the one beneath it. That has a cost the single-ground table hides. Text contrast falls as the surface rises:
| Surface | Primary #f2f5ff | Secondary #b4bdd6 | Tertiary #8d97b3 | Accent #4f7cff |
|---|---|---|---|---|
Ground #0b1020 | 17.38:1 | 10.09:1 | 6.50:1 | 5.10:1 |
Card #111830 | 16.11:1 | 9.35:1 | 6.02:1 | 4.73:1 |
Raised #18213f | 14.52:1 | 8.43:1 | 5.43:1 | 4.26:1 |
Popover #212b4d | 12.71:1 | 7.38:1 | 4.75:1 | 3.73:1 |
The tertiary grey that had a comfortable 6.50:1 on the ground is 4.75:1 on the popover, close to the line. The accent that read 5.10:1 as link text on the ground is 4.26:1 on the raised surface and 3.73:1 on the popover, so it stops passing for normal-size link text after the card. Check each text level on the lightest surface that holds it.
- Choose three or four surface steps, a few points of lightness apart.
- A hairline border a little lighter than the surface separates cards. If it is not needed to identify a control, it is decoration. If it is the only sign that an input exists, it must reach 3:1 against the adjacent colours. On this palette
#4a5370gives 2.49:1 on the ground, so a border in that grey would fail for an input, while#6b7594gives 4.14:1 on the ground and 3.84:1 on the card. - Do not rely on colour alone for state. Pair a colour change with a shape, underline, icon or text change. Frames, brackets and borders in interface design develops this.
In a user-selected forced-colors mode (forced-colors: active), MDN lists that box-shadow and text-shadow are forced to none, and that background-color, border-color and outline-color are among the properties whose colours the browser overrides. If a card's edge matters, draw it with a real border or outline, not only with a shadow.
Dimming text with opacity
Opacity looks convenient, since one white can serve every level. The cost is that the result depends on the layer below. Take #f2f5ff and blend it, in display-encoded sRGB, over the ground:
| Opacity | Resulting colour | Ratio on #0b1020 |
|---|---|---|
| 90% | #dbdee9 | 14.10:1 |
| 70% | #adb0bc | 8.76:1 |
| 60% | #9699a6 | 6.68:1 |
| 50% | #7e8390 | 4.99:1 |
| 40% | #676c79 | 3.60:1 |
That same 60 percent white over the popover surface #212b4d resolves to #9ea4b8, which is 5.57:1 against that surface. If you must use opacity, measure it on every surface it will meet, or define solid colours per level and avoid the question.
Glow and bloom
Glows read well on a dark ground and are an easy way to make a dark UI feel considered. They are also an easy way to damage it.
- Never put a glow behind body text. Contrast checkers compare the text colour with the background colour and ignore
text-shadow, but eyes do not: a bright halo around a letter lowers the effective difference between the letter and its surroundings. - Keep glows on decorative elements: hairlines, large headings, focus rings, particles and orbs.
- Limit blur radius and intensity, and use a colour close to the element's own, not white. Compare the blurred result at 100 percent zoom on a laptop panel and on a phone.
- Remember overlap. Glows stack. Overlapping additive blue glows move toward white and can drift in hue; see a blue palette that does not turn purple.
Let the browser know
Declare the theme so form controls, scrollbars and default backgrounds match it:
:root { color-scheme: dark; }
MDN lists what the property changes: the canvas colour, the default colours of scrollbars and other interaction UI, form controls, and other browser-provided UI. Without it, a dark page can still get a light scrollbar or light form controls. If you offer both themes, use color-scheme: light dark and style with prefers-color-scheme or a user toggle.
Typography on dark
Dark backgrounds are less forgiving of small, thin, tightly spaced type. Allow a little more letter-spacing on small text, avoid weights below regular for body copy, and give paragraphs slightly more line-height than you would on a light ground. These are adjustments to try and judge, not measured requirements. Display type has more latitude. The advice in pairing a display serif with a geometric sans works on either ground, but check the thin strokes of a high-contrast serif at small sizes, where they can vanish. WCAG's definition of large-scale text carries a note that fonts with extraordinarily thin strokes are harder to read at lower contrast.
The states people forget
Most dark designs pass on the default state and fail on the others. Placeholder text in an input is text and falls under 1.4.3, so the pale grey that signals "empty" often fails. Selected rows, inactive tab labels and chart axis labels are all text on a background and all measurable. Focus is the one to protect. 2.4.7 requires a visible keyboard focus indicator, and a ring clipped by a parent with overflow: hidden is an indicator problem under that criterion. 1.4.11 asks for 3:1 against adjacent colours for the indicator. 2.4.11 (AA) is about something else: the focused component must not be entirely hidden by author-created content, which a sticky header can cause. The AAA criterion 2.4.13 goes further and asks for an indicator at least as large as a 2 CSS pixel perimeter. The accent ring from the table measures 5.10:1 on the ground and 3.73:1 on the popover, so it clears 3:1 on both, though with far less margin on the lighter surface. Test each state, not only the resting one.
Testing
- Measure every text and component colour against its real background with a contrast checker, including hover, focus and any state that stays visible.
- View the page on at least one low-brightness phone and one ordinary laptop, in daylight and in the dark.
- Take screenshots and desaturate them. If hierarchy disappears, you relied on hue alone.
- Switch the operating system to a forced-colors or high-contrast mode and check that borders, focus rings and icons remain.
- Zoom to 200 percent and read the smallest text.
Checklist
- Ground off-black with a hue, text off-white; avoid pure
#000and#ffffor comfort and depth, knowing WCAG does not require it. - Three or four text levels, each a token with a measured ratio; nothing below 4.5:1 for normal text, measured on the lightest surface it sits on.
- Aim for about 6:1 on the dimmest regular text to leave margin.
- Depth through lighter surfaces; identify controls with 3:1 borders or other cues.
- Glow on decoration only, never behind paragraphs.
- Declare
color-schemeand test forced colours.
Questions and answers
What contrast ratio does dark mode need?
The same as light mode. WCAG 2.2 asks for at least 4.5:1 for normal text and 3:1 for large text, and 3:1 for the visible boundaries and states of interface components and meaningful graphics.
Is pure black a bad background?
It is not forbidden, and WCAG sets no maximum ratio. Black with white text gives the highest possible ratio, 21:1, but some readers report it as glaring, and a shadow or a raised surface cannot show against pure black. A very dark tinted grey or navy leaves room to build depth and is a common choice for long reading.
Can I use opacity to make dimmer text?
You can, but the resulting colour depends on what is behind the text, so the ratio changes when the background changes. A 60 percent white over a dark navy ground measures 6.68:1 and over a lighter card 5.57:1. Prefer solid colours for each text level and measure each against the background it sits on.
Does a glow or text-shadow count towards contrast?
No. Checkers compare the text colour with the background colour. A glow can help legibility on photographs, but it cannot be relied on to meet a ratio, and a strong glow behind text can reduce the real contrast.
Sources
- WCAG 2.2 (W3C Recommendation) w3.org
- WCAG 2.2: Understanding Contrast (Minimum) w3.org
- WCAG 2.2: Understanding Non-text Contrast w3.org
- MDN: color-scheme developer.mozilla.org
- MDN: forced-colors media feature developer.mozilla.org