css-hidden-focus

Focusable elements must not be visually hidden

manualWCAG 2.4.7 (AA)confidence: lowseverity: seriousoperable

Checks that keyboard-focusable elements are not visually hidden by CSS techniques that can leave them in the tab order.

Applies to. Applies to elements that are tabbable (keyboard-focusable) but are visually hidden via CSS techniques that can leave them in the tab order.

Expectation. No element should be tabbable while visually hidden (e.g., opacity:0, clipped, off-screen). An element that CSS brings back into view when it takes focus is not hidden while focused, and is not flagged: the usual skip-link pattern (.skip { position: absolute; left: -9999px } .skip:focus { left: 0 }), or a hiding rule that stops applying on focus (.visually-hidden-focusable:not(:focus) { clip: rect(0 0 0 0) }). A focus rule nested in another (.skip { &:focus { top: 0 } }) counts as written out; one whose focus is inside :has() counts, for its subject, as :focus-within does. A focus rule that isn't !important doesn't override an !important declaration that hides the element.

What a finding reports.

  • metrics.visibilityHints: how the element is hidden while it can take focus: opacityZero, offscreen, clipped or zeroSizeOverflowHidden (no size, with overflow hidden) by its own styles; when they show it, outOfReach (a container is positioned off screen, or, in a browser, the box focus would show lies where no scrolling reaches) or containerClipped (a container is clipped to nothing, has no size with overflow hidden, or is scaled to nothing, or, in a browser, the element lies wholly outside a container with overflow: clip, which focus doesn't scroll).
  • runtimeProbe: null unless focusing the element moved focus straight to another element (cssHiddenTabbable_runtimeRedirect_needsReview). Then it holds redirected: true and the tag and id of the element focus moved to (redirectedToTag, redirectedToId). Only the first three hidden elements are tried this way.

These data.details fields help to read and reproduce a finding. Apart from reasonCode and the few fields Output schema names as stable, they are not a stable contract: a field may be renamed or dropped in a minor release, so don’t build on them.

Examples

can't tell
<a href="/x" style="opacity:0;">Hidden but tabbable link</a>

The link stays in the tab order despite opacity:0 making it invisible to sighted keyboard users.

n/a
<a href="/x">Visible link</a>

The link is both focusable and visible — nothing to flag.

n/a
<style>.skip { position: absolute; left: -9999px; } .skip:focus { left: 0; }</style>
<a class="skip" href="#main">Skip to content</a>

The link is off-screen at rest, but its :focus rule brings it back into view when it takes focus.

How to check

When this rule can’t decide, it reports cantTell and leaves the question to a person. Each question it can ask is listed here under its reason code, with the steps to answer it. A finding carries the same steps as howToCheck, in the scan’s language. See Output schema.

cssHiddenTabbable_needsFocusStateVerification

  1. Click just before the element the report names, then press Tab until focus should reach it.
  2. Check document.activeElement in the developer tools console to be sure focus is on it.
  3. Look for the element and its focus indicator on screen.

Pass. The element comes into view with a visible focus indicator while it has focus.

Fail. Focus lands on the element while it stays hidden, so a keyboard user cannot see where focus is, which fails WCAG 2.4.7.

cssHiddenTabbable_runtimeRedirect_needsReview

  1. Click just before the element the report names, then press Tab until focus should reach it.
  2. Watch where focus lands and check document.activeElement in the developer tools console.
  3. Press Shift+Tab from the control after it and see whether focus stops on the hidden element or passes straight on.

Pass. Focus never stays on the hidden element: it passes straight on to a visible control, as a focus trap's sentinel does.

Fail. Focus can stay on the hidden element, so a keyboard user cannot see where focus is, which fails WCAG 2.4.7.

View source