Colour contrast is the difference in brightness between text (or a graphic element) and its background, expressed as a ratio from 1:1 (no contrast) to 21:1 (black on white). WCAG, the web accessibility standard, requires at least 4.5:1 for normal text and 3:1 for large text and user interface components at level AA, and 7:1 at level AAA. It is not an aesthetic preference: a light grey on white that looks elegant to you is unreadable for a real share of visitors, and Lighthouse, axe and any audit will flag it.
This article explains how the ratio is calculated, what each criterion requires, how to measure it and how to fix the typical cases without redoing the palette.
How the ratio is calculated
The formula is in the WCAG definition:
ratio = (L1 + 0.05) / (L2 + 0.05)
where L1 is the relative luminance of the lighter colour and L2 that of the darker one, both between 0 (black) and 1 (white). Luminance comes from the sRGB components: each channel is normalised to 0–1, linearised (if ≤ 0.04045 it is divided by 12.92; otherwise ((c + 0.055) / 1.055) ^ 2.4) and combined with the weights 0.2126 R + 0.7152 G + 0.0722 B. Green weighs far more than blue because the human eye is more sensitive to it: that is why a saturated blue on black contrasts less than it seems.
In JavaScript:
function luminance([r, g, b]) {
const lin = (c) => {
c /= 255;
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
};
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
}
function contrast(a, b) {
const [l1, l2] = [luminance(a), luminance(b)].sort((x, y) => y - x);
return (l1 + 0.05) / (l2 + 0.05);
}
contrast([29, 29, 31], [255, 255, 255]); // #1d1d1f on white ≈ 16.8:1
contrast([110, 110, 115], [255, 255, 255]); // #6e6e73 on white ≈ 5.1:1
contrast([134, 134, 139], [255, 255, 255]); // #86868b on white ≈ 3.6:1 (fails for normal text)
The three examples are this blog’s greys: the first is the main text, the second the grey we use for secondary text (passes AA) and the third the Apple grey we started with and had to drop for small text.
What each criterion requires
| Criterion | Level | Applies to | Minimum ratio |
|---|---|---|---|
| 1.4.3 Contrast (Minimum) | AA | Normal text | 4.5:1 |
| 1.4.3 Contrast (Minimum) | AA | Large text (≥ 24 px, or ≥ 18.66 px bold) | 3:1 |
| 1.4.6 Contrast (Enhanced) | AAA | Normal text | 7:1 |
| 1.4.6 Contrast (Enhanced) | AAA | Large text | 4.5:1 |
| 1.4.11 Non-text Contrast | AA | User interface components (field borders, required icons, focus indicators) and informative graphics | 3:1 |
Exempt: decorative text, logos, text in disabled controls and text that is part of a purely decorative image. A form field placeholder is not exempt: it is text the user needs to read.
“Large” is defined in points (18 pt, or 14 pt bold); in CSS that is roughly 24 px and 18.66 px.
How to measure it
- Colour tool on AIMRAN Tools: enter the text colour and the background colour (in HEX, RGB, HSL or OKLCH) and it shows the ratio and whether it passes AA and AAA for normal and large text. It runs in the browser.
- Chrome DevTools: when inspecting text, the colour picker in the Styles panel shows the ratio and two lines on the colour wheel marking where AA and AAA begin. The CSS Overview panel lists every low-contrast pair on the page.
- Lighthouse / axe: the
color-contrastaudit lists the failing elements with their current ratio. - Firefox: the accessibility inspector has a contrast check with colour-blindness simulation.
Automatic tools do not catch text over images or gradients: there you have to measure against the lightest (or darkest) area of the background that can end up under the text.
The most frequent failures and how to fix them
- “Subtle” grey for secondary text.
#999on white gives 2.9:1. Go down to#6e6e73(5.1:1) or#595959(7:1). Hierarchy also comes from size and weight, not only colour. - White text on light brand colours. White on
#0071e3gives 4.7:1 (passes); on#34c759(green) only 2.2:1. For labels in light colours, use dark text or mix the colour with black: on this blog, category labels usecolor-mix(in srgb, var(--c) 62%, black)for text and the pure colour only in icons and backgrounds. - Links distinguished by colour alone. Besides contrast with the background, a link inside a paragraph must differ from normal text (WCAG asks for 3:1 between the two or an additional cue, such as underlining). Underlining is the simplest.
- Light placeholders. The default grey in many browsers does not reach 4.5:1. Set
::placeholder { color: #6e6e73 }. - Invisible focus indicator. The keyboard
outlinemust have 3:1 against the adjacent background. Aoutline: 2px solidin the primary colour on a white background usually passes; on a button of the same colour it does not: add anoutline-offsetso the ring sits over the background. - Dark mode not reviewed. Greys that work on white do not work on
#000. Measure both palettes separately.
A note on APCA
WCAG 3, still a draft, is studying a different algorithm (APCA) that better accounts for polarity (light text on dark versus dark on light) and font size. It is an interesting design reference, but the legal requirement and the one audits measure today is the WCAG 2 ratio, so that is the one you must meet.
Conclusion
Contrast is calculated with a fixed formula from the luminance of the two colours and compared with three thresholds: 4.5:1 for text, 3:1 for large text and components, 7:1 if you aim for AAA. Measure with DevTools or a contrast tool, review light and dark separately and remember that visual hierarchy does not depend on grey alone: size, weight and spacing do the same job without leaving anyone out.