axe-core color-contrast wcag aa 4.5:1 failure
Fixes the axe-core color-contrast failure by adjusting failing foreground/background pairs to meet the WCAG AA 4.5 to 1 ratio. Use it when axe reports insufficient color contrast in a scan or CI gate. Not for contrast complaints that axe does not flag, like text over images.
axe-core color-contrast wcag aa 4.5:1 failure - how to fix it
TL;DR
Darken the text or lighten the background until the pair hits 4.5 to 1 for normal text (3 to 1 for large text). Check the failing pair with a contrast checker, pick the smallest change that passes, and re-run axe. One line of why: the ratio is computed from relative luminance, so small hex tweaks near the threshold usually fix it without a redesign.
The error, verbatim
{
"id": "color-contrast",
"impact": "serious",
"help": "Elements must meet minimum color contrast ratio thresholds",
"nodes": [
{ "target": [".muted-caption"], "failureSummary": "Fix any of the following: Element has insufficient color contrast of 2.91 (foreground color: #9a9a9a, background color: #ffffff, font size: 12.0pt, font weight: normal). Expected contrast ratio of 4.5:1" }
]
}Fix it step by step
Step 1: Reproduce on one page
npx @axe-core/cli https://example.com --rules color-contrast --save axe-contrast.jsonExpected: axe-contrast.json lists color-contrast violations with the actual ratio, foreground, and background colors per node.
Step 2: List every failing pair
node -e "const r=require('./axe-contrast.json'); r.violations[0].nodes.forEach(function(n){console.log(n.target.join(' '), n.any[0].data.fgColor, 'on', n.any[0].data.bgColor)})"Expected: A list of selectors with their failing color pairs, so you can fix tokens instead of one-offs.
Step 3: Verify the fixed ratio offline
python3 -c "def lum(h): c=[int(h[i+1:i+3],16)/255 for i in (0,2,4)]; c=[((v+0.055)/1.055)**2.4 for v in c]; return 0.2126*c[0]+0.7152*c[1]+0.0722*c[2]
def r(a,b):
x,y=lum(a),lum(b); return round((max(x,y)+0.05)/(min(x,y)+0.05),2)
print(r('#595959','#ffffff'))"Expected: Prints 7.0 or higher, confirming the new pair passes AA before you touch the stylesheet.
Step 4: Apply the token change and re-scan
npx @axe-core/cli https://example.com --rules color-contrastExpected: Exit code 0, 0 violations. Spot-check the page visually since darker muted text is the most common side effect.
Step 5: Gate the rule in CI
npx @axe-core/cli https://example.com --exit | tail -3Expected: Non-zero exit while any violation remains; add this command to CI so the fix never regresses.
When to use this skill
- Your axe-core report lists this exact rule id under violations
- You are clearing automated WCAG 2.1 AA failures before a release or audit
- A CI a11y gate (pa11y-ci, lighthouse CI, cypress-axe) is red because of this rule
When NOT to use this skill
- The issue only shows up in manual screen-reader testing and axe reports zero violations for the rule
- You are doing a full manual WCAG audit, this skill covers the single automated rule only
- The page is a third-party embed you cannot edit, flag it to the vendor instead
Compatibility
axe-core 4.8+ (rule color-contrast, WCAG 1.4.3). Works the same under pa11y, lighthouse (uses the same contrast math), and axe DevTools. Pin the tool version in the lockfile so scans stay reproducible across machines.
Variant phrasings
contrast ratio of 2.91 expected 4.5:1
The per-node failureSummary wording. Same fix: adjust the pair until the computed ratio clears 4.5.
axe text contrast failure on muted gray text
The usual real-world shape: secondary text in light gray on white.
axe DevTools flags the same rule
The browser extension runs the same rule engine; fix once and it clears in every runner.
Why it happens
Axe computes contrast from the relative luminance of the rendered foreground and background colors, including layered backgrounds and opacity. Design tokens for secondary or placeholder text are often picked by eye on a designer's calibrated monitor and land around 2.5 to 3.5 to 1, which fails the 4.5 to 1 AA bar for normal text. Transparency and background images make it worse because the effective background is darker or busier than the token suggests. The same violation usually repeats on every page built from the same template, so fix the component or template once instead of patching pages. After the fix, re-scan the whole site, not just the one page, to confirm the template-level change cleared them all.
Edge cases
- Large text (18pt or 14pt bold and up) only needs 3 to 1, so bumping a failing caption to large text is a legit fix, not a hack.
- Text over gradients or images cannot be measured reliably, axe may pass it while it still fails for users, add a scrim or solid backdrop.
- Focus indicators and disabled controls have their own rules, do not try to fix those with the text contrast ratio.
- Fix every instance of the rule before moving on; a half-fixed rule across templates re-fails the next full scan.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_teZsuGYtQB2DLnHWhQoD1w