Colour, contrast and accessible visual design
Contrast ratios for text and non-text content, not relying on colour alone, focus indicator visibility, dark mode, and a palette approach that passes by construction.
The ratios that matter
| Content | Minimum ratio | Level | Notes |
|---|---|---|---|
| Body text | 4.5:1 | AA | Measured against the background behind the text |
| Large text (18.66 px bold or 24 px) | 3:1 | AA | Large means a specific size, not a subjective impression |
| UI component boundaries | 3:1 | AA | Input borders, checkbox outlines, toggles |
| Graphical objects | 3:1 | AA | Icons that carry meaning, chart lines |
| Focus indicator | 3:1 against both states | AA | The indicator and the component it surrounds |
| Body text, enhanced | 7:1 | AAA | Aspirational for long-form reading |
| Placeholder text | 4.5:1 | AA | It is text; the browser default usually fails |
/* a palette that passes by construction, not by inspection */
:root {
--text: #1a1a1a; /* 16.1:1 on white */
--text-muted: #595959; /* 7.0:1 on white - still AA for body text */
--accent: #14509b; /* 8.6:1 on white, so it works as link and button text */
--border: #767676; /* 4.5:1 on white - passes as a UI boundary */
--focus: #b34a00; /* distinct hue, 4.6:1 on white, never colour-only */
}
:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px; /* keeps the indicator clear of the component edge */
}
/* never remove the outline without replacing it */
button:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }- Contrast is measured against what is actually behind the text, including an image, a gradient or a translucent panel - not against the nominal page background.
- Grey placeholder text is the most commonly failed element on forms. If the instruction matters, it belongs in a label, not a placeholder.
- Disabled controls are exempt from the contrast requirement, but making them unreadable is still a usability problem - use opacity sparingly.
Never colour alone
<!-- a status shown with colour, an icon and text -->
<p class="status status-error">
<svg aria-hidden="true" focusable="false" width="16" height="16"><!-- exclamation --></svg>
<strong>Payment failed.</strong> The card was declined by the issuer.
</p>
<!-- a chart with a pattern as well as a colour, and a legend that is text -->
<figure>
<figcaption>Monthly revenue by region, 2026</figcaption>
<svg role="img" aria-labelledby="chart-title chart-desc">
<title id="chart-title">Monthly revenue by region</title>
<desc id="chart-desc">Europe rises steadily; North America is flat; Asia doubles in June.</desc>
</svg>
</figure>| Meaning conveyed | Colour-only failure | Add |
|---|---|---|
| Error state | A red border | An icon, a message, and aria-invalid |
| Required field | A red asterisk | The word "required" in the label |
| Chart series | Six similar hues | Patterns, direct labels, or distinct shapes |
| Link in body text | A different colour only | An underline |
| Legend | Colour swatches | Text labels beside each swatch |
| Progress | Green to red | A number or a percentage |
Colour blindness affects roughly one in twelve men. It is not only red and green: deuteranopia and protanopia shift many pairs toward each other, including orange and green, and blue and purple. Testing a screenshot through a simulation takes a minute.
Dark mode and custom themes
@media (prefers-color-scheme: dark) {
:root {
--text: #f2f2f2; /* 15.4:1 on #121212 */
--text-muted: #b8b8b8;
--accent: #8ab4f8; /* lightened, because the dark blue failed on dark */
--border: #8c8c8c;
--focus: #ffb37a;
}
/* images designed for light backgrounds often need a tweak */
img.illustration { filter: brightness(0.9) contrast(1.05); }
}
/* and honour a user who has asked for more contrast */
@media (prefers-contrast: more) {
:root { --text-muted: var(--text); --border: #000; }
}- A palette built for light backgrounds usually fails in dark mode. Accents in particular become too dark; test every combination again rather than inverting.
- Do not rely on the operating system to invert anything. Assume the browser will honour your colours and check them.
- Custom themes chosen by the user need the same contrast checks as the defaults. A theme picker without a contrast constraint ships inaccessible options.
⚠️
Removing the focus outline and replacing it with a subtle background change is the single most common accessibility regression in a redesign. A focus indicator must be visible against both the component and its surroundings, and it must not be the only cue for the current item in a list or a set of tabs.
FAQ
Should I design the palette first or check it afterwards?
Design it to pass from the start. Choosing colour values that already meet the ratios avoids a late redesign, and the constraint rarely makes the result look worse.
Is a contrast checker enough?
It tells you whether a pair passes, not whether the design communicates. Pair it with a colour-blindness simulation and always add a second non-colour cue.
Related
WCAG in plain language Images, media and alternative text
Last refreshed 2026-09-18.