Type something to search...
How much colour contrast does text need for WCAG?

How much colour contrast does text need for WCAG?

Under WCAG 2.2 Level AA, normal text needs a colour contrast ratio of at least 4.5:1 against its background, and large text needs at least 3:1. Large text means at least 24 CSS pixels, or at least about 18.66 CSS pixels when bold. The stricter Level AAA raises these to 7:1 for normal text and 4.5:1 for large text. The ratio is calculated from the relative luminance of the two colours and ranges from 1:1 (identical colours) to 21:1 (black on white). Logos, purely decorative text and text on disabled controls are exempt, but almost everything else people need to read must pass.

Low contrast is consistently among the most common accessibility failures found on websites, and it affects far more people than those with diagnosed vision impairments: anyone reading on a phone in sunlight, on a cheap monitor or with tired eyes benefits from stronger contrast. In this article you'll learn exactly what the WCAG thresholds are, how the contrast ratio is calculated, which text is covered and which is exempt, how to test colours, and how to build a palette that passes from the start.

The WCAG Contrast Requirements

Two success criteria deal with text contrast:

CriterionLevelNormal textLarge text
1.4.3 Contrast (Minimum)AA4.5:13:1
1.4.6 Contrast (Enhanced)AAA7:14.5:1

A related criterion, 1.4.11 Non-text Contrast (AA), requires 3:1 for the visual boundaries of user interface components, such as input borders and focus indicators, and for parts of graphics needed to understand them. It doesn't cover text itself, but it often matters in the same components.

Level AA is the target referenced by most accessibility laws and procurement standards, including the European standard EN 301 549. Level AAA is a worthwhile goal for long-form reading content, but WCAG itself notes that it isn't possible to meet every AAA criterion for all content.

What Counts as Large Text

WCAG defines large text as at least 18 point, or at least 14 point bold. For the web, WCAG treats 1 point as 1.333 CSS pixels:

  • Large text: 24px or larger at normal weight.
  • Large bold text: About 18.66px or larger at a bold weight (700 or above).

These are the computed sizes in CSS, before any user zoom. A 20-pixel heading at weight 400 is not large text and needs 4.5:1. The same heading at weight 700 is large text and needs only 3:1.

How the Contrast Ratio Is Calculated

The ratio compares the relative luminance of the lighter colour (L1) and the darker colour (L2):

contrast ratio = (L1 + 0.05) / (L2 + 0.05)

Relative luminance is a value from 0 (black) to 1 (white). To calculate it from an sRGB colour:

  1. Divide each of the red, green and blue values by 255 to get a number from 0 to 1.
  2. Linearise each channel: if the value is 0.04045 or less, divide it by 12.92; otherwise, raise ((value + 0.055) / 1.055) to the power of 2.4.
  3. Combine them: L = 0.2126 × R + 0.7152 × G + 0.0722 × B.

Green contributes most to perceived brightness and blue the least, which is why a bright blue can still fail on black while a yellow passes easily.

A Contrast Function in JavaScript

You can implement the formula in a few lines. This works in any modern browser or in Node.js:

function hexToRgb(hex) {
  const value = hex.replace("#", "");
  const full = value.length === 3
    ? value.split("").map((c) => c + c).join("")
    : value;
  return [0, 2, 4].map((i) => parseInt(full.slice(i, i + 2), 16));
}

function luminance(hex) {
  const [r, g, b] = hexToRgb(hex).map((channel) => {
    const c = channel / 255;
    return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
  });
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function contrastRatio(foreground, background) {
  const a = luminance(foreground);
  const b = luminance(background);
  const [lighter, darker] = a > b ? [a, b] : [b, a];
  return (lighter + 0.05) / (darker + 0.05);
}

const pairs = [
  ["#000000", "#ffffff"],
  ["#767676", "#ffffff"],
  ["#777777", "#ffffff"],
  ["#595959", "#ffffff"],
  ["#ffffff", "#2563eb"],
];

for (const [fg, bg] of pairs) {
  const ratio = contrastRatio(fg, bg);
  const aa = ratio >= 4.5 ? "pass" : "fail";
  console.log(`${fg} on ${bg}: ${ratio.toFixed(2)}:1 (AA normal text: ${aa})`);
}

Running it with node contrast.js prints:

#000000 on #ffffff: 21.00:1 (AA normal text: pass)
#767676 on #ffffff: 4.54:1 (AA normal text: pass)
#777777 on #ffffff: 4.48:1 (AA normal text: fail)
#595959 on #ffffff: 7.00:1 (AA normal text: pass)
#ffffff on #2563eb: 5.17:1 (AA normal text: pass)

Notice how fine the margin is: #767676 passes on white and #777777, one step lighter, fails. WCAG treats the thresholds as exact, so 4.48:1 is a fail. Don't round up.

What the Requirement Covers

The contrast criteria apply to text and images of text, including:

  • Body copy, headings and links: All the obvious reading text.
  • Text in buttons, tabs, badges and navigation: Measured against the button or element background.
  • Form labels, help text and error messages: Including text inside inputs that users type.
  • Placeholder text: If it conveys information users need, it must meet the ratio. Most default browser placeholder greys don't.
  • Text over images and gradients: It must meet the ratio against the background behind it, which means checking the lightest or darkest part it overlaps.
  • Text in each state: Hover, focus, visited and selected states must all pass.

What Is Exempt

WCAG lists a small number of exceptions:

  • Logotypes: Text that is part of a logo or brand name.
  • Incidental text: Text in inactive (disabled) user interface components, text that is pure decoration, text that isn't visible to anyone, and text that is part of a picture with significant other visual content, such as a street sign in a photograph.

Disabled buttons are exempt, but they still need to be identifiable as controls, and many teams choose to keep their text readable anyway.

Testing Contrast in Practice

Browser Developer Tools

Chrome, Edge and Firefox all show contrast information in their developer tools. In Chrome, inspect a text element, click the colour swatch next to the color property in the Styles pane and look for the Contrast ratio line. It shows the ratio with ticks for AA and AAA and draws lines on the colour picker marking where the thresholds are, so you can drag to the nearest passing colour.

Automated Checkers

Tools such as axe, Lighthouse and WAVE scan a whole page and flag text that fails. You can also run axe from the command line in a CI pipeline:

npx @axe-core/cli https://example.com --tags wcag2aa

Automated tools have limits. They can't always work out the background behind text on images, gradients or semi-transparent layers, and they may report those as needing review rather than as failures. Check those cases by hand.

Semi-Transparent Colours

If text or its background uses transparency, contrast depends on what's underneath. Text at rgba(0, 0, 0, 0.5) on white blends to a mid-grey of about #808080, which fails at just under 4:1. It's usually clearer to use solid colours for text and reserve transparency for decorative layers.

Building a Palette That Passes

It's far easier to choose passing colours up front than to fix them later. Define text colours as tokens and check each pairing once:

:root {
  --color-bg: #ffffff;
  --color-surface: #f3f4f6;

  --color-text: #111827;        /* 17.7:1 on white, 16.1:1 on surface */
  --color-text-muted: #4b5563;  /* 7.6:1 on white, 6.9:1 on surface */
  --color-link: #1d4ed8;        /* 6.7:1 on white */
  --color-danger: #b91c1c;      /* 6.5:1 on white */

  --color-on-primary: #ffffff;
  --color-primary: #1d4ed8;     /* white text on this: 6.7:1 */
}

body {
  color: var(--color-text);
  background-color: var(--color-bg);
}

.button-primary {
  color: var(--color-on-primary);
  background-color: var(--color-primary);
}

Some patterns help keep a palette accessible:

  • Have one muted text colour, not five: Every extra grey is another pairing to check. One well-chosen secondary grey covers most needs.
  • Test against every background it appears on: A grey that passes on white may fail on a light grey card.
  • Darken brand colours for text: A bright brand colour often fails as text on white. Keep it for large elements and use a darker shade for links and small text.
  • Check dark mode separately: Contrast is symmetrical, but your dark palette is a different set of pairings and must be tested on its own.

Don't Overshoot in Dark Themes

Maximum contrast isn't always most comfortable. Pure white text on pure black can produce a glowing, blurry effect for some readers, particularly people with astigmatism. Off-white on a very dark grey, such as #e5e7eb on #111827, still gives a ratio well above 7:1 while being gentler to read. There's more on this in how dark mode affects typography.

Beyond WCAG 2: APCA

WCAG 3 is being developed by the W3C and is still a draft. Work on it has explored a different contrast method, the Accessible Perceptual Contrast Algorithm (APCA), which takes font size and weight into account more directly and treats light-on-dark differently from dark-on-light. APCA isn't part of any current standard, and the methods in WCAG 3 may change before it's finalised. For legal and compliance purposes, WCAG 2.x contrast ratios remain the requirement. Some teams use APCA as an extra check for readability, particularly for dark themes and thin fonts.


FAQ: Text Colour Contrast and WCAG

At least 4.5:1 for WCAG 2.2 Level AA, and at least 7:1 for Level AAA. Most body text is below the large text threshold, so the normal text ratio applies.

No. WCAG treats the thresholds as exact minimums, so anything below 4.5:1 fails for normal text, even by a small margin. Adjust the colour until the ratio is at or above the threshold.

For normal text at Level AA, about #767676, which gives 4.54:1. One step lighter, #777777, falls to 4.48:1 and fails.

No. Text that is part of a logo or brand name is exempt from the WCAG contrast criteria. Other text near the logo, such as a tagline used as content, is not exempt.

Yes, if it conveys information. Many browsers' default placeholder colours are too light. It is better to put essential information in a visible label rather than relying on placeholder text.

Yes. The same ratios apply to any colour scheme you offer. Test your dark theme pairings separately, because they are different colours from your light theme.

No. APCA has been explored for WCAG 3, which is still a draft. Current laws and standards refer to WCAG 2.x, so the 4.5:1 and 3:1 ratios remain the requirement.


Conclusion

WCAG's text contrast requirements are short and precise: 4.5:1 for normal text and 3:1 for large text at Level AA, rising to 7:1 and 4.5:1 at AAA, where large means 24 pixels or about 18.66 pixels bold. The ratio comes from a fixed luminance formula, the thresholds are exact, and they apply to every piece of readable text, including placeholders, buttons and text on images, with exemptions only for logos and incidental text.

The practical approach is to settle on a small set of colour tokens, check each foreground and background pairing once, and use developer tools and automated scanners to catch regressions. Darken brand colours for text, keep muted greys to a single value that clearly passes, and test dark themes on their own. Do that, and contrast stops being a recurring audit finding and becomes part of how your typography works.

Share :

Related Posts

How to make typography accessible?

How to make typography accessible?

You make typography accessible by choosing clear typefaces, setting text in relative units so it scales with user preferences, giving it enough colou

Dive Deeper
What are the parts of a letterform?

What are the parts of a letterform?

A letterform is the shape of a single letter, and typographers break it down into named parts. The main ones are the stem (the main vertical

Dive Deeper
What are ascenders, descenders and the baseline?

What are ascenders, descenders and the baseline?

The baseline is the invisible line that letters sit on. Ascenders are the parts of lowercase letters that rise above the x-height, such as th

Dive Deeper