How to fix
Fixing contrast that only breaks when you tab to it
A button reads perfectly well sitting on the page. You press Tab, it takes focus, and the text all but disappears. Every automated accessibility checker says the button passes, because every automated checker measures the page as it sits. The people who hit the problem are the ones navigating by keyboard, who only ever see the focused state.
What the failure looks like
The usual cause is a focus style that changes colour without checking the result. Something like this is very common:
.btn { background: #0d5c54; color: #ffffff; } /* 7.8:1, fine */
.btn:focus { background: #eeeeee; color: #ffffff; } /* 1.2:1, invisible */
At rest the label is white on dark teal, comfortably past the 4.5:1 minimum. On focus the background lightens and the label stays white, so the text is now white on near-white. Nobody notices during design review, because a mouse user never sees it. A keyboard user sees nothing else.
The same thing happens in reverse on light themes: dark text on a pale background, focus repaints the background dark, and the text is now dark-on-dark.
Why your accessibility checker says it passes
This is not a bug in the tools. It is a gap in what they measure.
axe-core is the engine behind most accessibility testing, including ours. It has exactly two contrast rules: color-contrast for the AA minimum and color-contrast-enhanced for AAA. Both read the computed style of the element as the page currently stands. Neither one focuses anything.
There is no focus-contrast rule in axe at all. So a control that fails only when focused is not a violation that gets suppressed or downgraded, it is a violation the check never had the opportunity to see. The same is true of overlay widgets, which cannot alter your stylesheet and so cannot fix or detect it either.
WCAG covers it perfectly well. 1.4.3 Contrast (Minimum) applies to text in the state it is presented, and a focused control is presented focused. The criterion is not ambiguous; the tooling simply was not looking.
How to fix it
Change the focus indicator, not the text contrast. The safest focus style adds something rather than repainting something:
.btn:focus-visible {
outline: 3px solid #ffbf47;
outline-offset: 2px;
}
An outline sits outside the control, so it cannot affect the contrast between the label and its background. It is also visible against both light and dark surroundings if you pick the colour with a little care.
If you do want the colours to change on focus, then check the pair you are changing them to, exactly as you would for the resting state. Both of these are fine:
/* darken the background, keep the light text */
.btn:focus-visible { background: #06403a; color: #ffffff; } /* 10.9:1 */
/* invert cleanly */
.btn:focus-visible { background: #ffffff; color: #0d5c54; } /* 7.8:1 */
Two practical notes. Prefer :focus-visible to :focus so the indicator appears for keyboard users without also firing on every mouse click. And never write outline: none without putting a visible indicator back, which is the single most common way focus styling gets broken in the first place.
How we test it, and how we got it wrong first
We added this check because axe cannot do it, and the first implementation was wrong in a way worth describing, because it is the obvious way to build it.
The obvious approach is to read the CSS. Find every rule whose selector contains :focus, strip the pseudo-class, test whether the element matches what is left, and apply those declarations. It is fast, it touches nothing, and it is easy to reason about.
It reported failures on six of fourteen real shops whose appearance does not change on focus at all.
Two reasons. It approximated the cascade by document order, so it lost every specificity contest and "applied" declarations that the real page overrides. And it walked into @media blocks without checking whether the query matched, so a focus colour defined inside @media print counted as though it applied on screen.
A false pass tells somebody their site is fine when it is not. A false failure tells them to go and fix something that was never broken. Both are the same underlying sin, which is asserting more than you measured.
So the browser decides now. Each candidate control is genuinely focused, and the computed style is read back afterwards. If the browser reports the same colours focused as unfocused, there is nothing to report, whatever the stylesheet appears to say. CSS is still parsed, but only to choose which elements are worth focusing, where guessing generously costs nothing because the measurement no longer trusts it.
Because that mutates the page, the pass runs last, after everything else has been measured, and puts the scroll position and the previously focused element back when it finishes.
What this check deliberately will not tell you
Four honest limits, because a check you cannot calibrate is not much use:
- Focus styling applied by JavaScript, a class toggled on
focusinrather than a CSS pseudo-class, is not detected. - Stylesheets served from another domain cannot be read, so a control whose only focus rule lives in a third-party sheet may never be chosen as a candidate.
- A focus state over a background photo is left alone rather than estimated.
- Only controls you can reach by pressing Tab are considered. An element with
tabindex="-1"can be focused by script but a keyboard user cannot land on it, so a focus-only problem there is not a barrier.
It also says nothing about whether your focus indicator is visible enough in the WCAG 2.2 sense (2.4.11 Focus Appearance). That is a different measurement, the contrast of the indicator against its surroundings rather than of the text against its own background, and we do not claim it.
Why it is worth checking
Keyboard navigation is the one thing that has to work for a large group of people who cannot use a pointer at all, and the focus state is the only feedback they get about where they are. A control whose label vanishes at exactly the moment it is selected is a worse experience than one that was never styled.
It is also cheap to fix once you can see it: usually a single declaration in one rule. The hard part has only ever been finding it, because the tooling was measuring the wrong state. You can see the focused state for your own pages with the free scan, or run it in CI with the CLI and GitHub Action.
Frequently asked questions
Why does axe-core not report this?
axe has two contrast rules, color-contrast and color-contrast-enhanced, and both measure the element as the page currently stands. Neither focuses anything, and there is no focus-contrast rule in axe, so a control that fails only when focused is never evaluated in that state.
Which WCAG criterion does this fall under?
1.4.3 Contrast (Minimum), the same criterion as ordinary text contrast. It applies to text in the state it is presented, and a focused control is presented focused. The thresholds are unchanged: 4.5:1 for normal text, 3:1 for large text.
Should I use :focus or :focus-visible?
:focus-visible in most cases. It shows the indicator when someone is navigating by keyboard without also firing on every mouse click, which is the behaviour that led people to remove focus styles in the first place.
Is removing the outline the problem?
Only if nothing replaces it. outline: none with a considered replacement indicator is fine. outline: none on its own removes the only signal a keyboard user has, and it is how most missing focus styles begin.
Related guides
See what's actually broken on your site
Real axe-core results, every element outlined. No email wall, no fake “compliant” badge.
Run a free scanLast updated 2026-08-03.