Skip to content

AccessibilityPart of: Web accessibility

WCAG color contrast: the ratios, and how to fix low-contrast text

WCAG color contrast asks for a ratio of at least 4.5:1 for normal text and 3:1 for large text, buttons and focus rings. Here is how the ratio works, the colours that fail most and how to fix them once in your theme.

getReport teamUpdated 4 Oct 202615 min read

WCAG color contrast comes down to two numbers: text needs a contrast ratio of at least 4.5:1 against its background, and large text, buttons, input borders, meaningful icons and focus rings need at least 3:1. Those are the level AA minimums in WCAG 2.2, and low-contrast text is the finding almost every accessibility check reports first. It is usually caused by three or four colours, so fixing them in one place clears dozens of elements at once.

This guide explains how the ratio works, the colours that fail most and how to fix them in your theme without a redesign. Contrast is one of the ten fixes in the web accessibility guide, which puts it in context with the rest. Budget an hour for the first pass.

Quick answer

WhatMinimum ratio, WCAG 2.2 AAAAA (enhanced)
Normal text: body copy, captions, labels, placeholders4.5:17:1
Large text: 24 px and up, or 18.66 px (14 pt) and up when bold3:14.5:1
Buttons, input borders, icons that carry meaning, focus indicators3:1 against what is next to them-
Logos, disabled controls, pure decorationNo requirement-
  • Run the accessibility checker and open the contrast finding: it lists the elements below the ratio.
  • Fix the colour in your theme's palette or CSS variables, not on each element.
  • Mid-grey text on white (#999999) is 2.85:1 and fails. #767676 is the lightest grey that passes on white (4.54:1).
  • Text over photos needs a dark overlay (a scrim); no tool can measure it reliably, so check it by eye.

Why color contrast accessibility matters

About 1 in 12 men and 1 in 200 women have reduced colour vision (figures from Colour Blind Awareness), and many more people have low vision from age or cataracts. For them, text at 2.5:1 is not "a bit faint", it is unreadable. And everyone reads a phone in the sun sometimes.

It also costs sales: the grey "Free delivery over €50" line and the pale "Add to cart" label are exactly the text that sells.

And it is a legal baseline. WCAG 2.2 is the current W3C Recommendation (October 2023), also published as ISO/IEC 40500:2025. The European Accessibility Act, in force since 28 June 2025, points through EN 301 549 to WCAG AA: v3.2.1, still the version cited for the law, uses WCAG 2.1, and v4.1.1 (September 2026) moves to 2.2. The contrast criteria are identical in both: 1.4.3 Contrast (Minimum) for text and 1.4.11 Non-text Contrast for controls. WCAG explained covers the levels and versions.

How the contrast ratio is computed

Each colour gets a relative luminance: how bright it looks to the eye, from 0 (black) to 1 (white). Green counts the most, red less, blue very little, which is why pure blue text on black is much harder to read than pure green. The ratio is the lighter luminance plus 0.05, divided by the darker luminance plus 0.05. It runs from 1:1 (the same colour twice) to 21:1 (black on white).

Three practical consequences:

  • No rounding. 4.49:1 fails. If one tool shows 4.5 and another 4.48, trust the one with more decimals.
  • Hue does not help. A red and a green of the same brightness have a ratio near 1:1 even though they look different to most people. Contrast is about light and dark.
  • Transparency is blended first. Text at opacity: 0.6, or an rgb(0 0 0 / 0.5) colour, is measured as the colour that actually appears on screen.

How getReport checks it

The report opens the page in Chromium and runs axe-core, which works out the foreground and background colour of every visible text element and compares the ratio with 4.5:1 or 3:1 depending on font size and weight. The contrast finding collects every element that fails:

The contrast finding opened: the title counts the text elements below the required ratio, and the technical detail names the color-contrast rule and lists the CSS selectors of the affected elements
Each selector is one failing element; the same class repeating down the list means one CSS rule to fix.

Three things the card does not tell you:

  • The same elements are also counted in the serious violations. axe rates colour contrast as serious, so fixing the colours clears both findings.
  • Links that differ from body text only by colour are a separate axe rule (link-in-text-block, WCAG 1.4.1). They appear under serious violations, not under contrast.
  • "Needs review" elements are not listed. When axe cannot work out the background (text over an image, a gradient, a video, or an element partly covered by another), it marks the element for a human check rather than failing it. getReport lists only confirmed failures, so a clean contrast finding says nothing about your hero banner.

Pale icon buttons are often also tiny ones, and the fix happens in the same CSS file, so the tap-target finding is worth reading next to this one. The colour contrast fix page has the short reference version.

How to fix low-contrast text, step by step

1. Group the failures by colour, not by element

Open the finding and look at the selectors. A list like .product-card__meta, .product-card__meta, .footer a, .footer a, input::placeholder is not five problems; it is two or three colours.

In Chrome or Edge: right-click the text → Inspect → in the Styles pane, click the colour square next to color. The picker shows the ratio with AA and AAA ticks, and a line across the colour field: anything on the safe side passes. Firefox has the same in its Accessibility panel.

2. Pick passing colours from a known-safe list

Measured with the WCAG formula:

TextBackgroundRatioResult
#999999 grey#ffffff white2.85:1Fails, even for large text
#767676 grey#ffffff white4.54:1Passes for all text, barely
#767676 grey#f5f5f5 light grey4.17:1Fails for normal text
#595959 grey#ffffff white7.0:1Passes with room (AAA)
#ffffff white#0066cc blue5.57:1Passes
#ffffff white#1a73e8 blue4.51:1Passes, barely
#ffffff white#e53935 red4.23:1Large text only
#ffffff white#d32f2f red4.98:1Passes
#ffffff white#28a745 green3.13:1Large text only
#ffffff white#1e7e34 green5.14:1Passes
#ffffff white#ff9800 orange2.16:1Fails
#000000 black#ff9800 orange9.74:1Passes

Two patterns worth remembering. A grey that passes on white can fail on the light grey of a card or footer, so check each background you use. And bright orange, yellow and light green buttons need dark text, not white.

3. Fix it once, in your colour tokens

If colours are written out in dozens of places, the fix will not stick. Put them in CSS custom properties on :root in the theme's main stylesheet and use the variables everywhere:

CSS
/* theme stylesheet, e.g. assets/css/main.css */
:root {
  --color-text: #1f1f1f;        /* 16.5:1 on white */
  --color-text-muted: #595959;  /* 7.0:1 on white, 6.4:1 on #f5f5f5 */
  --color-link: #0066cc;        /* 5.6:1 on white */
  --color-surface: #ffffff;
  --color-surface-alt: #f5f5f5;
  --color-button-bg: #0066cc;
  --color-button-text: #ffffff; /* 5.6:1 on the button */
}

body { color: var(--color-text); background: var(--color-surface); }
.meta, .caption, .help-text { color: var(--color-text-muted); }
a { color: var(--color-link); }

/* Placeholders are styled separately, and some browsers dim them */
::placeholder { color: var(--color-text-muted); opacity: 1; }

A brand colour that fails does not have to go. Keep it for large headings, borders and backgrounds with dark text, and use a darker shade of the same hue for small text and links, a "text-safe" variant one step down the palette.

4. Put a scrim behind text on images

Text on photos passes or fails depending on which part of the photo is behind each letter, and the photo changes when someone swaps the banner. Put a semi-transparent dark layer between the image and the text:

CSS
/* hero banner with a background image and white text */
.hero { position: relative; }
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 0.6); /* white text stays at 5.7:1 or more, whatever the photo */
}
.hero__content { position: relative; color: #ffffff; }

A 60 % black layer over the brightest possible pixel (pure white) gives #666666, and white text on #666666 is 5.74:1. At 50 % it drops to 3.95:1, enough only for large text. If the design needs more of the photo, use a gradient that is dark only where the text sits.

5. Check the states nobody screenshots

Hover and focus colours, error messages, "sold out" and "sale" badges, cookie banners, and the text inside form fields after someone types all need the same ratios. Axe checks the page as it loads, so hover and focus states are yours to check by hand.

Focus indicators fall under the 3:1 rule for interface components. A thin light-blue glow on a white page usually fails; a solid two-pixel outline in a dark colour with a small offset works on almost any background. Keyboard navigation has the full focus-ring pattern.

CSS
:focus-visible {
  outline: 2px solid #1f1f1f;
  outline-offset: 2px;
}

A link that is blue while the text around it is dark grey is invisible as a link to someone who cannot tell those colours apart. Underline links inside paragraphs; menus and buttons, where position already says "link", can do without.

CSS
main p a, main li a { text-decoration: underline; text-underline-offset: 0.15em; }

7. Re-run the check

Run the checker again, then on a product page, a blog post and the checkout, because each template can have its own colours.

How to build an accessible color palette

Fixing failures one by one works, but a palette built to pass from the start stops them coming back. An accessible color palette is one where every colour has a job and every text-and-background pair you allow meets WCAG 2.2: 4.5:1 for normal text, 3:1 for large text, and 3:1 for UI components such as input borders, meaningful icons and focus rings.

  1. List your backgrounds first. White, the light grey of cards and footers, a tinted panel, a dark header. Every colour is judged against each background it will sit on.
  2. Give each hue a scale of shades, light to dark, and decide the role of each shade: light ones are backgrounds (with dark text on them), a middle one can be a border or icon at 3:1, and only shades that reach 4.5:1 on every background where text appears are allowed for text and links.
  3. Write down the allowed pairs, in the design system or as comments next to the CSS variables, so nobody has to measure again.
  4. Never rely on colour alone. WCAG 1.4.1 asks for a second signal: an error is red and has an icon or the word "Error"; a link in a paragraph is blue and underlined.

A small palette that passes, measured with the WCAG formula:

RoleColourOn #ffffffOn #f5f5f5
Body text#1f1f1f16.48:115.12:1
Muted text#5959597.00:16.42:1
Links, buttons with white text#0066cc5.57:15.11:1
Success text#1e7e345.14:14.71:1
Error text#d32f2f4.98:14.57:1
Input borders and icons, never text#8a8a8a3.45:13.17:1

The success green shows why step 1 matters: on a light blue panel (#e8f0fe) it drops to 4.48:1 and fails for normal text.

Verify every pair against the W3C definition rather than a palette tool's badge: the formula is in WCAG 2.2 under "contrast ratio" and "relative luminance", and the thresholds in 1.4.3 and 1.4.11. Use a checker that shows two decimals, since 4.49:1 fails, then run the accessibility checker on real pages, where the palette meets real backgrounds.

Platform notes

WordPress block themes. Colours live in the global styles: Appearance → Editor → Styles → Colors edits the palette stored in theme.json under settings.color.palette. Blocks refer to palette entries by name, so darkening one entry fixes every block that uses it. When you pick a text and background colour for a block, the colour panel warns "This color combination may be hard for people to read"; take the hint.

WordPress classic themes and builders. Appearance → Customize → Colors, or the theme's options panel; hard-coded greys can be overridden in Customize → Additional CSS. Page builders (Elementor, Divi) let every widget override the global palette; set global colours in the builder's site settings, then hunt for local button colours.

Shopify. Online Store → Themes → Customize → Theme settings → Colors. Current themes use colour schemes (text, background, button, button label) per section. A scheme with white text on a light accent fails in every section that uses it, so fix the scheme.

Static sites and custom builds. Search the stylesheets for hard-coded colours (grep -rnE "#[0-9a-fA-F]{3,6}" src/styles) and replace them with tokens. In Tailwind's default palette, gray-400 and lighter fail as text on white; gray-500 (#6b7280 in Tailwind 3, and close to it in Tailwind 4's OKLCH palette, about 4.8:1) is the lightest default grey that passes.

Verify

  • The contrast finding reads "Text has enough colour contrast" on each template you checked.
  • Text over images reads clearly on the brightest photo you have.
  • Tab through the page: every focused element has a ring you can spot without searching.
  • For what no contrast tool can judge, do a short manual pass with a screen reader in 20 minutes.

Common mistakes

  • Fixing each element instead of the colour. The next blog post brings the problem back. Change the palette or the CSS variable.
  • A grey tested on white, used on grey. #767676 passes on white and fails on #f5f5f5.
  • "Disabled" buttons that are not disabled. Inactive controls are exempt only when they really are inactive (disabled attribute). A pale "Continue" button that still works must pass.
  • A text shadow to rescue low contrast. The ratio is measured on the text and background colours; the shadow does not count.
  • Placeholder text as the only hint. Placeholders must meet 4.5:1 too, and they vanish on typing; form labels explains what to use instead.

Questions people ask

What is a contrast ratio?

A contrast ratio compares the brightness of two colours, usually text and its background, on a scale from 1:1 (identical) to 21:1 (black on white). WCAG computes it from each colour's relative luminance, so hue does not count, only light and dark. Normal text needs 4.5:1 at level AA, and large text and controls need 3:1.

What counts as large text in WCAG?

Large text is at least 18 point (24 CSS pixels) at normal weight, or at least 14 point (about 18.66 CSS pixels) in bold. Large text only needs a contrast ratio of 3:1 at level AA instead of 4.5:1, because bigger strokes stay readable with less contrast. Measure the rendered size, not the size in the design file.

Does placeholder text need to meet contrast requirements?

Yes. Placeholder text is text, so it needs 4.5:1 against the field's background, which the default light grey in most browsers and themes does not reach. Set ::placeholder to a passing colour with opacity: 1. Better still, put instructions in a visible label or hint, because a placeholder disappears once someone starts typing.

Do logos and disabled buttons need colour contrast?

No. WCAG exempts text that is part of a logo or brand name, purely decorative text, and controls that are genuinely inactive. The exemption only holds when the button really is disabled, with the disabled attribute or aria-disabled="true". A pale button that still submits the form is an active control and must meet the ratio.

What contrast ratio does WCAG AAA require?

Level AAA, criterion 1.4.6 Contrast (Enhanced), asks for 7:1 for normal text and 4.5:1 for large text. Most laws and contracts ask for AA, not AAA, so treat 7:1 as a target for body text where the design allows it. #595959 on white reaches exactly 7:1, a useful dark grey for body copy.

Will WCAG 3 change the contrast rules?

Not yet. WCAG 3 is still a W3C working draft with no fixed contrast method, and newer models such as APCA are not part of any standard a law refers to. WCAG 2.2's 4.5:1 and 3:1 ratios remain the measure for the European Accessibility Act, EN 301 549 and US settlements, so meet those first.

Check your site before and after Check