Website Colour Accessibility: Contrast Ratios Explained
Colour contrast is the lightness difference between a foreground element and what sits directly behind it. On a website, that usually means text against a surface, but it also includes icons, chart marks, focus indicators and the visible edges of controls. A contrasting palette is not automatically accessible: every important foreground and background pairing needs checking in its actual state.
What a contrast ratio measures
WCAG contrast uses relative luminance, not a simple difference between hex values or a colour’s saturation. It compares the lighter value (L1) and darker value (L2) with the formula (L1 + 0.05) / (L2 + 0.05). The result ranges from 1:1 for identical luminance to 21:1 for black and white. It is written as a ratio, so #FFFFFF on #005A9C and the reverse pairing have the same ratio, although their roles in an interface differ.
This is why a hex code has no single accessible or inaccessible status. A blue that works for white button text may fail for a white page background. Treat each token as a role within a pair: body text on a canvas, link text on a canvas, label text on a button, and icon stroke on its component surface.
The thresholds worth designing to
For WCAG 2.2 Level AA, ordinary text and images of text need at least 4.5:1 against the background. Large-scale text can use 3:1. WCAG describes large-scale text as at least 18 pt regular, or 14 pt bold, which is commonly about 24 CSS pixels regular or 18.7 CSS pixels bold. Do not rely on a large-text allowance for a thin, condensed or unusually shaped typeface: its actual strokes may still be difficult to read.
Level AAA raises those targets to 7:1 for ordinary text and 4.5:1 for large-scale text. AAA can be a useful target for long-form reading and critical instructions, while AA remains the practical baseline for most interface text.
Contrast is not only a typography task. Active user-interface components and graphical objects that people must identify, such as input boundaries, selected states and meaningful chart lines, need 3:1 contrast against adjacent colours at AA. Inactive controls, incidental decoration and logotypes have exceptions, but an exception is not a reason to make a usable control hard to see.
Build usable pairings, not a colour list
Start a palette by naming functional tokens rather than assigning colours to vague labels such as “secondary”. For a light interface, define a canvas, strong text, subdued text, link or action, action text, border and focus colour. Then test the combinations each component actually renders. A subdued text token may be acceptable for supporting copy yet unsuitable for a button label.
:root {
--canvas: #FFFFFF;
--ink: #172033; /* 16.27:1 on canvas */
--text-subtle: #5F6B7A; /* 5.43:1 on canvas */
--action: #005A9C; /* 7.14:1 with white */
--action-text: #FFFFFF;
}
body { background: var(--canvas); color: var(--ink); }
a { color: var(--action); }
.button { background: var(--action); color: var(--action-text); }
Here, --ink is comfortably above AAA for body text, --text-subtle meets AA for ordinary text, and the button label meets AAA. Keep the calculated ratio alongside the token in design documentation or code comments, then re-check it when a brand colour or surface changes.
Do not use colour as the only carrier of meaning. An error state needs text, an icon, a field message or another visible cue in addition to an error colour. Links embedded in paragraphs should be identifiable without hue alone, for example with an underline that remains on hover and focus. A 3:1 difference in lightness can distinguish an element in some cases, but it does not replace a separate cue where users need to know that a field is valid or invalid.
Worked colour pairs
- Body copy:
#172033on#FFFFFFis 16.27:1. It passes AA and AAA for ordinary text. - Supporting copy:
#5F6B7Aon#FFFFFFis 5.43:1. It passes AA for ordinary text but not AAA. Use it deliberately, not for low-priority information people still need to read. - Primary button:
#FFFFFFon#005A9Cis 7.14:1. It passes AAA for ordinary button text. - Common failure:
#FFFFFFon#3B82F6is 3.68:1. It fails AA for ordinary-sized text. It only clears the 3:1 large-scale-text threshold, so use a darker action background or a darker label colour for normal button text. - Dark interface text:
#F8FAFCon#0F172Ais 17.06:1. It passes AAA for ordinary text and is a dependable starting pair for a dark canvas.
These values are calculated from the unblended solid colours shown. If a colour has alpha transparency, calculate the resulting composited colour against the real surface first. For a translucent black overlay on an image, that surface may vary across the image, so checking only the intended background colour gives a misleading result.
Test states and real backgrounds
Check the screen users actually encounter, not just an isolated style tile. A reliable review covers default, hover, focus, active, selected, disabled and error states; light and dark themes; and responsive layouts where a label wraps or moves onto a different surface. Test placeholder text, helper text, badges, table rows and chart keys too. The most polished-looking low-contrast failure often appears in a quiet state that was never part of the primary mock-up.
Gradients, photography and textured surfaces need extra care. Sample the lightest and darkest areas behind text, or add a solid or sufficiently opaque backing layer so that the text always has a known background. Avoid placing essential copy directly over a variable image merely because it works at one crop. Borders and focus rings should also remain visible next to nearby surfaces, not just against the page canvas.
Use a contrast checker early, then repeat the check in the browser after CSS effects, transparency and component states are applied. Pair numerical checking with keyboard and zoom testing: a passing ratio improves legibility but does not ensure that a focus indicator, error message or interactive target is understandable.
Use palette inspiration responsibly
For interface starting points, inspect the verified Cloud Console, Axiom, Tidal UI and Midnight SaaS palettes, then calculate the particular pairs you adopt. The Modern SaaS and Minimal & Monochrome collections can help with structure, but aesthetic direction is never evidence of conformance.
Brand pages such as Stripe, Google and BBC are editorial colour references only. They are not endorsed by those brands, not official brand guidelines and not accessibility standards. Re-test any borrowed pairing in your own interface and content context.
Further reading
- W3C: Understanding Success Criterion 1.4.3, Contrast Minimum
- W3C: Understanding Success Criterion 1.4.1, Use of Color
- MDN: Colour contrast
- Material Design: Colour contrast
Before publishing, run every foreground and background state through the contrast checker. Then explore the Modern SaaS palette collection and adapt only the pairs that pass in your finished interface.
Related guides
- How to Choose a Colour Palette for a Website
- Colour Palettes: How to turn a colour palette into CSS variables
- How to Use a Five-Colour Palette Without Using Every Colour Equally