( Perceivable / WCAG 1.4.3 )
Text contrast fails after a hover, focus, or state change
What is this issue?
A text element’s color passes WCAG’s 4.5:1 minimum (or 3:1 for large text) in its resting, default state, but a CSS state selector (:hover, :focus, :active, :disabled, or a .dark-mode class) changes its color or background so the resulting ratio in that state falls below the threshold. The failure is state-dependent: the same text node passes at one moment and fails at another, depending purely on what the user is doing to it.
This differs from a plain contrast failure in scope, not mechanism: it’s still measured with the exact same luminance-ratio math as Text does not have enough colour contrast, just evaluated across every reachable state instead of only the one visible on page load.
Why does this matter?
A contrast failure that only appears on hover or focus hits users at the precise moment they’re actively engaging with an element: reading a link they’re about to click, checking a form field they just tabbed into, confirming a button label before pressing it. A resting-state pass gives no protection here, because the user isn’t looking at the resting state anymore.
Dark mode compounds the risk further: colors picked and tested against a white background often behave unpredictably against near-black, since contrast ratio depends on the actual rendered pair, not on either color in isolation. A theme toggle that seemed like a purely visual feature can silently reintroduce contrast failures a team already fixed once for light mode.
Who is affected?
- Low vision users: need the same reliable brightness difference at every interaction state as at rest; a link or button that passes contrast until the exact moment it’s hovered or focused loses the readability a user depends on right when they need it most.
What users experience
Diane has low vision and relies on a mid-range contrast setting rather than maximum zoom, since heavy magnification makes her lose context on long pages. On a pricing page, plan-comparison links pass contrast normally at 6.8:1, but the site’s :hover state shifts the link color to a lighter accent blue that measures 2.9:1 against the same white background. As she moves her cursor down the list comparing plans, each link fades toward illegibility the instant she points at it, forcing her to look away from the cursor and re-locate her place by memory instead of by sight.
How do I fix it?
Test contrast for the color pairing that actually renders in every state a text element reaches (default, hover, focus, active, disabled, and any theme variant), not only the one visible when the page first loads. This works because contrast ratio is calculated from whatever colors are actually rendered at a given moment; a state that changes either color changes the ratio, independent of whatever passed before it.
Build this into the design system rather than checking it page by page: define hover and focus color tokens with contrast already verified against their expected backgrounds, the same way base text tokens are chosen, so new components inherit a passing combination by default instead of each author re-deriving one. For dark mode specifically, re-run a full contrast pass against the dark theme’s actual background colors; don’t assume ratios verified on light mode transfer.
Code Examples
/* Default: #333333 on #ffffff ≈ 12.6:1 - passes easily */
.plan-link {
color: #333333;
}
/* Hover: #b0b0b0 on #ffffff ≈ 2.3:1 - fails */
.plan-link:hover {
color: #b0b0b0;
}/* Default: #333333 on #ffffff ≈ 12.6:1 - passes */
.plan-link {
color: #333333;
}
/* Hover: #1a56c4 on #ffffff ≈ 6.1:1 - passes, and reads as an interactive cue */
.plan-link:hover {
color: #1a56c4;
text-decoration: underline;
}The fix keeps a real, visible hover state (the color still changes so a mouse user gets feedback) but chooses a hover color that clears 4.5:1 against the same white background, rather than fading toward it. Adding the underline gives a second, non-color cue on hover as well, so the interactive signal doesn’t depend on the color shift alone.
Common Mistakes
Mistake: “We ran a contrast checker on the page and everything passed, so we’re done.” A contrast checker run against a static screenshot or a page’s default HTML only evaluates the states rendered at that instant. Hover, focus, and active styles never activate during a passive scan, so their contrast is never measured unless someone deliberately triggers each state and re-checks.
Mistake: “Our hover color is lighter, but it’s just a subtle visual accent, not the main text.” If the lighter color is applied to text a user reads or a link label they need to identify, it’s still subject to the 4.5:1 minimum regardless of how subtle the visual intent was; WCAG doesn’t distinguish “accent” text from “primary” text in this requirement.
Mistake: “Dark mode uses the same color tokens as light mode, so if light mode passes, dark mode does too.” Contrast ratio depends on the actual rendered background, and a color that reads as muted grey on white can render very differently against near-black. Every color pairing needs its own contrast check per theme; token reuse across themes doesn’t guarantee ratio reuse.
How RedFlag Detects This
AI-assisted: flagged by the optional Review Labels feature. State-dependent contrast can’t be evaluated by a single-pass DOM scan, since the failing color only exists once a hover, focus, or other state is actively triggered. RedFlag’s Review Labels feature surfaces state-dependent contrast as a candidate for human review rather than running deterministic color math against every possible CSS state combination.
False negative: if a reviewer never triggers a particular state during review (an :active style only visible for the instant a mouse button is held down, for example), a failing state can go unflagged entirely, since nothing here re-derives every CSS selector combination automatically. False positive: a reviewer can misjudge a borderline ratio by eye without measuring it, flagging a pairing that’s actually just above the 4.5:1 line. Manual step: for any state flagged or suspected, trigger it directly (hover, tab to focus it, toggle dark mode) and measure the rendered color pair with a contrast checker rather than relying on visual impression alone.
Manual Testing
- Open the page in Chrome or Firefox with DevTools open to the Elements panel.
- For each text element with interactive states, use DevTools’ “Force element state” toggle (the
:hovbutton in the Styles pane) to lock in:hover,:focus, and:activeone at a time. - With each state forced, click the color swatch next to the computed
colorvalue and read the contrast ratio DevTools reports against the current background. - Confirm each state’s ratio still meets 4.5:1 (or 3:1 for large text); a state that drops below the threshold fails independently of whether the default state passed.
- If the site has a dark mode or theme toggle, repeat steps 2–4 with that theme active. Do not assume a light-mode pass carries over.
Related WCAG Success Criteria
1.4.3 Contrast (Minimum): Text and images of text must maintain at least 4.5:1 contrast against their background (3:1 for large text). This rule is the same criterion as Text does not have enough colour contrast, evaluated across interactive and theme states instead of only the page’s default appearance.
Related Issues
Text does not have enough colour contrast is the static-state version of this exact criterion: RedFlag’s automated check, which only evaluates the page as it renders at scan time.
Text does not meet enhanced AAA contrast requirements applies the stricter 7:1 threshold; a state-dependent failure at AA is worth checking against the AAA bar too on pages targeting that higher standard.
Focus indicator does not meet minimum size or contrast covers the related but distinct requirement that a focus indicator itself (an outline or ring), not the text inside the focused element, meet its own size and contrast thresholds.
Content on hover or focus cannot be dismissed shares this rule’s focus on hover/focus-triggered states being under-tested by static scans, applied to dismissibility rather than color.
UI component has insufficient contrast covers the equivalent state-independent measurement for borders and icons rather than text.
Context change happens when element receives focus is a different focus-triggered failure mode worth checking on the same elements: an unexpected navigation instead of a color problem.
References
- W3C Understanding 1.4.3: Contrast (Minimum)
- WebAIM Contrast Checker
- WebAIM: Contrast and Color Accessibility
Frequently asked questions
Why does a static contrast checker miss this problem?
A static checker, and RedFlag's own automated color-contrast rule, evaluates the page in its default, unmodified state at scan time. Hover, focus, active, and dark-mode states only exist when a real user (or a simulated interaction) triggers them, so a scan that never hovers or focuses an element never sees the color it renders in that state.
Do disabled-state color changes need to meet the contrast minimum?
No. WCAG explicitly exempts inactive user interface components, so a greyed-out disabled button or field is not required to meet 4.5:1. The requirement re-applies the moment that control becomes active again.
Does dark mode need a completely separate contrast audit?
Yes. Colors chosen for a light theme rarely carry over correctly to a dark one: a mid-grey that passes 4.5:1 on white can fail badly on a near-black background. Treat each theme as its own full contrast pass rather than assuming one check covers both.
Is a color change on focus different from removing the focus indicator entirely?
Yes, they are separate problems. This rule concerns a focus style that exists but drops text below the contrast minimum once applied; a focus indicator that is missing or invisible entirely is a distinct failure covered by keyboard-focus-visibility checks, not a contrast measurement.
Can this failure happen without any custom hover or focus CSS at all?
Yes. Browser and OS default styles, such as visited-link purple or a default focus outline color, can interact with a page's background in ways the design never explicitly set, producing a state-dependent contrast failure nobody wrote CSS to cause.