Accessibility

Contrast: tools and thresholds

Contrast is a ratio between luminances: 4.5:1 for normal text and 3:1 for large text. WCAG 1.4.3 Contrast (Minimum), level AA, sets the thresholds.

Key points

  • Criterion 1.4.3 Contrast (Minimum), level AA, asks for 4.5:1 for normal text and 3:1 for large text.
  • Large text is at least 24 px, or 18.66 px in bold; below that size you need 4.5:1.
  • The ratio uses sRGB relative luminance: gradients and transparency must be measured on the effective colour.

Contrast is the ratio between the luminance of the text and the luminance of the background: criterion 1.4.3 Contrast (Minimum), level AA, asks for 4.5:1 for normal text and 3:1 for large text. A tool calculates the number; the threshold depends on the role of the element.

The thresholds

  • Normal text: 4.5:1 with 1.4.3, level AA; 7:1 with 1.4.6 Contrast (Enhanced), level AAA.
  • Large text: 3:1, where large means at least 24 px or 18.66 px in bold.
  • Components and icons: 3:1 with 1.4.11 Non-text Contrast, level AA, for borders, states and symbols that carry information.
  • Focus indicator: 3:1 between the focused and unfocused state, with a minimum area, for 2.4.13 Focus Appearance, level AAA.

The calculation in one line

node -e "const L=h=>{const c=[1,3,5].map(i=>parseInt(h.slice(i,i+2),16)/255).map(v=>v{const[x,y]=[L(a),L(b)].sort((m,n)=>n-m);return((x+0.05)/(y+0.05)).toFixed(2)};console.log('#767676 on white',R('#767676','#ffffff'));console.log('#777777 on white',R('#777777','#ffffff'))"

The first grey passes at 4.54:1, the second stops at 4.48:1 and is not enough for normal text. The difference between the two colours is one hex value, not a perception: that is why the check runs on the calculated value, not by eye. The formula uses the sRGB relative luminance defined in WCAG 2.2, a W3C Recommendation dated 5 October 2023, published under the W3C Document License.

Where the number misleads

Three cases distort the measurement. Text over an image or a gradient has no single background: measure the worst spot. Opacity and color-mix() change the effective colour, so the value in the CSS is not the value on screen. A thin 16 px font stays normal text even if the design treats it as large.

The tools

Lighthouse, Apache-2.0 licence, flags elements below the threshold on the rendered page and uses axe-core, MPL-2.0 licence, for the accessibility audit. The WebAIM Contrast Checker and the TPGi Colour Contrast Analyser compute the ratio of a colour pair. None of the three knows the role of the text: the choice between 4.5:1 and 3:1 stays with whoever writes the markup.

npx --yes lighthouse https://example.com --only-categories=accessibility --chrome-flags="--headless" --output=json --output-path=./a11y.json

The report lists nodes with insufficient contrast and the calculated ratio. An element that is not listed is not automatically compliant: text on canvas, closed shadow DOM and hover states are not analysed.

The EneUI catalogue

EneUI indexes the palettes, the accessibility category and shadcn's icons, MIT licence from the shadcn/ui project: an icon that carries information falls under 1.4.11 and needs 3:1. Checked on 1 November 2026 against 111,985 components.

The limitation

The contrast ratio does not measure legibility. It ignores the effective size after zoom, letter spacing, font rendering and the contrast loss perceived by someone with cataracts. Meeting 4.5:1 is the verifiable minimum, not proof that the text reads well.

Where to continue

The method for measuring a palette is in measured contrast in a UI palette; the focus indicator is in the invisible focus ring. The comparison of headless primitives is in Radix, Base UI and React Aria.

Frequently asked questions

Is 4.5:1 always enough?

No. It applies to normal text; large text stops at 3:1 and non-text components at 3:1. At level AAA the threshold for normal text rises to 7:1.

What counts as large text?

At least 24 px, or 18.66 px in bold. Below those sizes 1.4.3 asks for 4.5:1.

Is contrast measured on the CSS or on the screen?

On the colour actually rendered. Opacity, gradients and background images change the result, so the value written in the CSS is not enough.

Does Lighthouse find every contrast failure?

No. It analyses the rendered DOM and skips canvas, closed shadow DOM and dynamic states such as hover and focus. A clean report does not close the check.

Is black on white always the best choice?

It has the highest ratio, 21:1, but it is not always right for people with dyslexia or photophobia. The criterion sets a minimum, not a specific colour.