Focus Not Obscured by Sticky Headers (2.4.11)

Permalink to "Focus Not Obscured by Sticky Headers (2.4.11)"

WCAG 2.2 added SC 2.4.11 Focus Not Obscured (Minimum): when an element receives keyboard focus, it must not be entirely hidden by author-created content. Data UIs are full of sticky content — table headers, frozen first columns, filter toolbars, bulk action bars, cookie banners, chat launchers — and keyboard users moving through a long table regularly land on rows hidden underneath them. Auditing for 2.4.11 means moving focus through real content while sticky elements are active, at several zoom levels.

This guide belongs to focus indicator contrast auditing.

Spec reference

Permalink to "Spec reference"
  • SC 2.4.11 Focus Not Obscured (Minimum) (AA) — the focused component is not entirely hidden by author-created content.
  • SC 2.4.12 Focus Not Obscured (Enhanced) (AAA) — no part of the focused component is hidden.
  • Exceptions — content the user can move or dismiss without moving focus may be considered if the user opened it; sticky headers are not exempt.
  • CSS — scroll-padding on scroll containers and scroll-margin on targets.
Sticky layers that can hide focus Layers: a sticky page header at the top; a sticky filter toolbar below it; a sticky table header row; a sticky bulk action bar at the bottom; a cookie banner fixed at the bottom; the focused row must stay visible between them. Sticky layers that can hide focusPage headersticky top, 64 pxFilter toolbarsticky, 48 pxTable header rowsticky, 40 pxBulk action barsticky bottom, 56 pxCookie bannerfixed bottom, 120 px until dismissed
Every sticky layer reduces the safe area.

When to use — and when not to

Permalink to "When to use — and when not to"

Audit 2.4.11 on every page with sticky or fixed elements and keyboard-navigable content, especially long tables and grids.

Do not assume scrollIntoView solves it; it aligns elements to the container edge, under sticky headers, unless scroll padding is set. The misapplication: fixing only the table header and forgetting a bottom bulk action bar that appears when rows are selected.

Partial fixes versus complete fixes Comparison: fixing only one sticky element leaves focus hidden behind others; accounting for every sticky layer at top and bottom, at all zoom levels, fixes 2.4.11. Partial fixes versus complete fixes✗ Scroll padding for header onlyTested at 100% zoomBanner ignoredManual spot check✓ Padding for top and bottom stacksTested at 200% and 400%Banner dismissible, padding while shownAutomated overlap check

Annotated code example

Permalink to "Annotated code example"
html { scroll-padding-top: calc(var(--page-header-h) + var(--toolbar-h)); scroll-padding-bottom: var(--bottom-stack-h); }
.table-scroll { scroll-padding-top: var(--thead-h); scroll-padding-bottom: var(--bulkbar-h, 0px); }
tr:focus-within, td:focus { scroll-margin-block: 8px; }          /* breathing room */
// Playwright: assert the focused element is not fully covered
async function assertFocusVisible(page) {
  const covered = await page.evaluate(() => {
    const el = document.activeElement; const r = el.getBoundingClientRect();
    const pts = [[r.left + 2, r.top + 2], [r.right - 2, r.top + 2], [r.left + 2, r.bottom - 2], [r.right - 2, r.bottom - 2], [r.left + r.width / 2, r.top + r.height / 2]];
    return pts.every(([x, y]) => { const hit = document.elementFromPoint(x, y); return hit && !el.contains(hit) && hit !== el; });
  });
  expect(covered).toBe(false);                                  // fully covered = 2.4.11 failure
}
for (let i = 0; i < 60; i++) { await page.keyboard.press('Tab'); await assertFocusVisible(page); }

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Situation Risk Fix
Tab down a long table Row under sticky header scroll-padding-top
Rows selected, bulk bar appears Last rows under bar scroll-padding-bottom
Shift+Tab upward Row under page header Page-level padding
400% zoom Sticky stack fills viewport Unstick lower layers
Cookie banner shown Bottom content covered Dismiss control, padding
Sticky elements and 2.4.11 risk Matrix of sticky elements — page header, filter toolbar, table header, frozen first column, bulk action bar, chat launcher — against risk when moving down, moving up, moving sideways and at high zoom. Sticky elements and 2.4.11 riskMoving downMoving upSidewaysHigh zoomPage headerLowHighNoneHighFilter toolbarLowHighNoneHighTable headerLowHighNoneHighFrozen first columnNoneNoneHighMediumBulk action barHighLowNoneHighChat launcherMediumNoneMediumMedium
Audit every direction of movement.

Step-by-step implementation

Permalink to "Step-by-step implementation"
  1. Inventory sticky elements. Search CSS for position: sticky and position: fixed and list them per page, with heights.
  2. Walk the keyboard path. Tab through every interactive element, then use grid arrow keys in all directions, with sticky elements active.
  3. Automate overlap checks. Use elementFromPoint at the corners and centre of the focused element after each move.
  4. Fix with padding and margins. Set scroll-padding on each scroll container to the sum of its sticky layers; use scroll-margin for breathing room.
  5. Reduce sticky stacks. At high zoom or small heights, unstick lower-priority layers.

Integration context

Permalink to "Integration context"

Sticky group headers are covered in sticky group headers while scrolling, and sticky headers in sticky headers and first columns. Focus contrast audits are in auditing focus indicator contrast and screenshots in automating focus indicator screenshots with Playwright. Zoom testing is in zoom and reflow testing at 400 percent.

Gotchas

Permalink to "Gotchas"

Frozen columns and horizontal movement. Moving left in a grid can place the focused cell under a frozen first column; use scroll-padding-left on the grid’s scroll container.

Dynamic heights. Toolbars that wrap at narrow widths change height; update CSS variables with a ResizeObserver.

Custom scroll code. Virtualized grids often scroll programmatically; include sticky offsets in their scroll calculations.

Edge cases

Permalink to "Edge cases"
  • Partially obscured focus. 2.4.11 only fails when focus is entirely hidden, but partial obscuring is still poor; aim for 2.4.12.
  • User-opened overlays. A filter popover the user opened may cover content; it should close or move when focus moves elsewhere.
  • Print and reader views. Not affected, but check that sticky positioning does not break them.

Worked example

Permalink to "Worked example"

An audit of an accounting app found keyboard users losing focus under a three-layer sticky stack and a bottom bulk action bar. A Playwright script tabbing through 200 elements flagged 31 fully covered positions. The team set scroll-padding from measured heights, added scroll-margin to rows, unstuck the filter toolbar below 600 pixels of height and fixed the grid’s programmatic scroll to include offsets. The script then passed at 100%, 200% and 400% zoom.

Common mistakes

Permalink to "Common mistakes"
  • Fixing only the top header.
  • Testing only at 100% zoom.
  • Ignoring horizontal movement under frozen columns.
  • Programmatic scroll code without offsets.

Testing checklist

Permalink to "Testing checklist"

Testing it with a screen reader

Permalink to "Testing it with a screen reader"

Screen reader users rarely see the problem, but sighted screen reader users and magnifier users do. Run NVDA with a magnifier at 200% and move through the table; the magnifier follows focus, so a hidden focus shows as a magnified view of the sticky header. Then disable the screen reader and repeat with only the keyboard, watching the focus ring at every step.

Reporting 2.4.11 failures

Permalink to "Reporting 2.4.11 failures"

Write 2.4.11 bugs so they can be reproduced: the page, viewport size, zoom level, the sticky element involved and the exact key presses from a known starting point. Include a screenshot with the focused element’s outline marked even though it is hidden, or a short screen recording. Name the sticky element’s component so the right team owns the fix, and mention whether the failure is total (2.4.11) or partial (2.4.12).

Key points

Permalink to "Key points"
  • List every sticky and fixed element, top and bottom.
  • Walk keyboard paths in all directions at several zoom levels.
  • Fix with scroll padding and margins; shrink stacks at high zoom.
  • Automate the overlap check so regressions are caught.

Design system notes

Permalink to "Design system notes"

Expose sticky layer heights as CSS variables from layout components and apply scroll padding globally, so new sticky elements are accounted for automatically.

FAQ

Permalink to "FAQ"
Does partial obscuring fail 2.4.11?

No, only full obscuring; partial obscuring fails 2.4.12 (AAA).

Are cookie banners exempt?

No, unless the user can dismiss them without moving focus and opened them; fix with padding or dismissal.

Does scrollIntoView help?

Only when scroll padding is set.

How do I test automatically?

Check the focused element’s points with elementFromPoint.

Do frozen columns count?

Yes; horizontal movement can hide cells.

What zoom levels should I test?

At least 100%, 200% and 400%.

Permalink to "Related"

← Back to Focus Indicator Contrast Auditing