WCAG 2.2 AA color contrast audit
Last reviewed: 2026-11-10
Quick prompts
For anyone improving a site or app, whether a small-business owner or someone at a larger company practicing better prompts. Low-contrast text is hard to read for many people, including anyone on a phone in sunlight, and it is a common accessibility failure. Paste a prompt into any AI assistant that can see the site or repo.
-
"Check the text and button colors on this page of the site I work on. Flag anything hard to read against its background and say how far off it is. Report first, no changes." Why this prompt works: Scope: one page gets a careful answer instead of a vague sweep.
-
"Using the WCAG AA contrast rules (4.5:1 for normal text), list any text on the site I work on that fails. Show the colors and the ratio for each, then wait for my go-ahead." Why this prompt works: Give criteria: a named standard and number turns opinion into a pass/fail check.
-
"Does the site I work on stay readable in dark mode and when links or buttons are hovered or focused? Give me a plain-language list of problems before proposing any color changes." Why this prompt works: Naming the states to check (dark mode, hover, focus) covers spots a quick look misses.
Audit the whole interface's color contrast against WCAG 2.2 AA and fix failures with the smallest changes that preserve the design. Report every failure with numbers.
Hard limits
- Fix only contrast. No content, navigation, layout, or behavior changes. Keep semantic color meaning (error stays red-family, success green-family, brand stays brand) and existing hierarchy.
- Do not use shadows, outlines, glows, or opacity as substitutes for adequate contrast.
- Prefer correcting shared design tokens when that fixes several instances.
- Review PR only. No merge, production deploy, DNS, mail, or secret changes without the owner's explicit approval. A live-site audit is read-only: public pages, normal browser, polite pace, no form posts, no logins.
The thresholds
| What | Minimum (AA) | Criterion |
|---|---|---|
| Normal text | 4.5:1 | 1.4.3 |
| Large text: at least 24px regular, or 18.66px (14pt) bold | 3:1 | 1.4.3 |
| UI component boundaries needed to identify the control (input borders, checkbox edges, toggle tracks), and state indicators (focus ring, selected, checked) | 3:1 against adjacent colors | 1.4.11 |
| Graphics needed to understand content (chart lines, icons that carry meaning) | 3:1 | 1.4.11 |
| Exempt | Disabled controls, logotypes, pure decoration, text in photos that is incidental | 1.4.3 / 1.4.11 |
Ratio = (L_lighter + 0.05) / (L_darker + 0.05), using WCAG relative luminance from sRGB. Do not round up: 4.49:1 fails. APCA is not part of WCAG 2.2; you may note it, but pass/fail uses the WCAG ratio.
Related AA checks worth noting while you are in the states: focus must not be fully hidden behind sticky content (2.4.11). Focus Appearance (2.4.13) is AAA, so report it as advice, not a failure.
Inputs to gather
- Repo, start branch, PR base, build/serve commands, env var names (never values), deploy or build-minute limits (one push per task if metered).
- Theme mechanism: CSS variables,
prefers-color-scheme, adata-themeattribute, a stored preference key, high-contrast mode. - Route list with one example per template, routes off in this build, and test accounts for logged-in screens.
Step 1: Classify and sample
Templates matter more than URL counts: shared chrome repeats, so sample one page per template plus every unique component.
| Type | Contrast hot spots |
|---|---|
| Article / editorial | Muted metadata (dates, bylines), captions, blockquote text, link color inside body text, code blocks, table headers, pull quotes over images |
| Marketing / landing | Text over hero images and gradients, ghost buttons, badges, testimonial cards, footer small print, CTA on brand colors |
| Docs / KB | Syntax highlighting themes, inline code, callout boxes (note, warning), sidebar active item, search placeholder |
| E-commerce | Sale and strike-through prices, stock badges, swatch borders, rating stars, filter checkboxes, disabled "add to cart" (exempt only if truly disabled) |
| Dashboard / data | Chart series vs background and vs each other where color is the only key, gridlines that carry meaning, status pills, zebra rows, sparkline colors, heat maps |
| SaaS / app | Toasts, tooltips, modals over scrims, tab underlines, toggle tracks, skeleton loaders that hold text, selected rows |
| Forms flow | Field borders, placeholders, helper and error text, required markers, focus rings, checkbox and radio edges, step indicators |
Rendering model: SSR/static, read the production build. SPA, wait for hydration and visit client-side routes. Third-party widgets whose CSS loads after yours (search UIs, embeds, consent banners) need scoped selectors with enough specificity; do not restyle cross-origin iframes you do not control and list them as not verified.
Step 2: Measure
- Real browser only (Playwright with Chromium). Some hosts block script user agents, and computed styles need a rendering engine.
- Both or all themes. Set the theme the way the site does before paint (stored key plus
prefers-color-schemeemulation). - Walk visible text nodes, placeholders,
::before/::aftercontent, icon-only buttons, SVG fills and strokes, and borders of inputs, selects, textareas, checkboxes, radios, and switches. - Compute the painted color: resolve inherited CSS variables, composite every alpha layer (color alpha and
opacity) over the actual background stack, and for gradients or images take the lowest-contrast area under the text. Pseudo-elements can carry their own background, so do not assume the page background. - Force states in DevTools/Playwright without submitting anything: default, hover, focus-visible, active, selected/checked, disabled, invalid/error, success, open menus.
- Native controls draw parts the CSS does not expose; sample pixels from a screenshot when needed.
- Recompute borderline pairs (within about 0.2 of the threshold) by hand with the formula.
- Run axe-core
color-contrastas a cross-check, not the only check; it skips gradients, images, and many pseudo-elements and can give false alarms.
Step 3: Fix
- Change the token, not each use. Check every place the token is used so the fix does not break another pair.
- Move along the same hue (darker or lighter) to keep brand and meaning.
- Placeholder text: set the color and
opacity: 1(some browsers and libraries fade placeholders). - Focus rings must contrast with whatever they are drawn on, including dark cards inside a light page.
- Decorative dividers under 3:1 can stay if they are not the only way to identify a control; say so in the report.
Step 4: Recheck
Repeat the full walk on all affected routes, themes, and states. Re-run axe. Run the production build and existing tests.
Report format
- Scope: interface type(s), templates and URLs sampled, themes, viewport(s).
- Failures, one block each: selector or component, foreground, background (as painted), measured ratio, required ratio, where it appears, fix, post-fix ratio.
- States forced and their results.
- Hand-checked passes near the line, and measured-but-exempt items with the reason.
- Live site comparison if requested (read-only): which live failures the PR fixes, live-only differences.
- Not verified: routes, themes, states, third-party iframes.
- PR link, base, "not merged".
References
- WCAG 2.2: https://www.w3.org/TR/WCAG22/
- Understanding 1.4.3 Contrast (Minimum): https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html
- Understanding 1.4.11 Non-text Contrast: https://www.w3.org/WAI/WCAG22/Understanding/non-text-contrast.html
- Relative luminance definition: https://www.w3.org/TR/WCAG22/#dfn-relative-luminance
- Understanding 2.4.11 Focus Not Obscured (Minimum): https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html
- Understanding 2.4.13 Focus Appearance (AAA): https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance.html
- axe-core rules: https://dequeuniversity.com/rules/axe/
- MDN
prefers-color-scheme: https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme