Check colour contrast

Measure text and interface colours against the WCAG contrast thresholds, and fix the pairs that fail.

Last reviewed

Outcome

A table of every colour pair on a screen, with its measured contrast ratio and the threshold it needs, in which every row passes. The fixed colours are back in the design tokens, so the next screen inherits them.

Contrast is the difference in luminance between a foreground colour and the colour behind it. WCAG expresses it as a ratio from 1:1 (no difference) to 21:1 (black on white), computed as (L1 + 0.05) / (L2 + 0.05), where L1 is the relative luminance of the lighter colour and L2 of the darker one. The thresholds you will check against:

WhatLevel AALevel AAA
Body text4.5:17:1
Large text (at least 18pt, or 14pt bold)3:14.5:1
Interface components and meaningful graphics3:1—

The first two rows come from success criteria 1.4.3 and 1.4.6. The last row is 1.4.11, Non-text Contrast: input borders, focus indicators, icons that carry meaning, and the parts of a chart a person needs to read it.

When to use it

  • Before a screen or a component ships, and whenever its colours change.
  • When you add a colour to a palette or a design token.
  • When an accessibility audit or a user reports text that is hard to read.

When not to use it

  • For logos and purely decorative elements, which WCAG exempts. The text of a disabled control is exempt too.
  • As the whole accessibility review. Contrast is one criterion; it says nothing about focus order, labels or screen reader output.

Before you start

  • The screen. The screen or component in every state you will check: default, hover, focus, selected, error and disabled.
  • Exact values. The colour values from the code or the design file, not from a screenshot.
  • A checker. The Contrast Checker or any tool that takes two colour values and reports the ratio.

Steps

Step 1: List the colour pairs

Work from the screen, not from the palette. A palette tells you which colours exist; only the screen tells you which ones sit on top of which. Take one screen or one component state at a time. For every piece of text, note its colour and the colour directly behind it. For every control, note the colour of its boundary or fill against the colour around it. Include the states: hover, focus, selected, error, and placeholder text.

You should now have: a list of every foreground and background pair, per state.

Step 2: Measure each pair

Enter exact values from the code or the design file into the checker. Do not sample from a screenshot, because compression and anti-aliasing shift the colours. For text over an image or a gradient, measure against the lightest area behind the text for dark text, and the darkest area for light text. If any part fails, the text fails.

You should now have: a measured ratio for every pair.

Step 3: Record each pair against its threshold

Write each pair, its ratio and the threshold it needs in one table, using the thresholds above. Mark every row that fails.

You should now have: one table with every failing pair marked.

Step 4: Fix the failures and measure again

Darken or lighten one colour of the pair; usually the text colour moves, so the brand background stays. For text over images, add a solid or semi-transparent layer behind the text and measure again against that layer. Make text large enough to fall under the large-text threshold only when the design already calls for large text. Then put the fixed colours back into the design tokens.

You should now have: a table in which every row passes, and updated design tokens.

Worked example

A sign-up form on a white background (#FFFFFF).

  1. Pairs: label text #6B7280 on white; placeholder text #9CA3AF on white; input border #D1D5DB on white; error text #EF4444 on white; the primary button's white text on #3B82F6.
  2. Measured: the label is 4.8:1, the placeholder 2.5:1, the border 1.5:1, the error text 3.8:1 and the button text 3.7:1.
  3. Against the thresholds: the label passes 4.5:1. The placeholder, error text and button text fail 4.5:1 as body text; the border fails 3:1 for interface components.
  4. Fixes: the placeholder moves to #6B7280 (4.8:1); the error text to #B91C1C (6.5:1); the button to #1D4ED8 (6.7:1); the border to #6B7280, which clears 3:1. Each new value replaces the old one in the design tokens.

Checklist

  • Every text and control colour pair on the screen is in the table.
  • Hover, focus, selected, error and disabled states were measured, not only the default.
  • Values came from the code or the design file, not a screenshot.
  • Text over images was measured against its worst area.
  • Every row meets its threshold.
  • The fixed colours went back into the design tokens.

Common mistakes

  • Checking the palette instead of the screen. Two colours that pass on their own can fail when one sits on the other.
  • Only checking the default state. Focus rings, placeholders and error messages are where contrast usually fails.
  • Sampling from screenshots. Compression shifts the colour, so the ratio you measure is not the one users see.
  • Enlarging text just to pass. It changes the design to meet a number; change the colour instead.

Sources

  1. W3C. Understanding Success Criterion 1.4.3: Contrast (Minimum) (WCAG 2.2).
  2. W3C. Understanding Success Criterion 1.4.6: Contrast (Enhanced) (WCAG 2.2).
  3. W3C. Understanding Success Criterion 1.4.11: Non-text Contrast (WCAG 2.2).
  4. W3C. Web Content Accessibility Guidelines (WCAG) 2.2 — definitions of contrast ratio and relative luminance.