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-paddingon scroll containers andscroll-marginon targets.
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.
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 |
Step-by-step implementation
Permalink to "Step-by-step implementation"- Inventory sticky elements. Search CSS for
position: stickyandposition: fixedand list them per page, with heights. - Walk the keyboard path. Tab through every interactive element, then use grid arrow keys in all directions, with sticky elements active.
- Automate overlap checks. Use
elementFromPointat the corners and centre of the focused element after each move. - Fix with padding and margins. Set
scroll-paddingon each scroll container to the sum of its sticky layers; usescroll-marginfor breathing room. - 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%.
Related
Permalink to "Related"- Sticky group headers while scrolling — group headers
- Focus appearance 2.4.13 for grid cells — indicator appearance
- Zoom and reflow testing at 400 percent — zoom