The sticky header that eats your focus
Shift+Tab back up a form and the field you land on slides under the sticky header. The browser scrolled just far enough, and just far enough is behind the bar. WCAG 2.2 calls it focus not obscured. One CSS property fixes it.
A keyboard user is filling in a form. They press Shift+Tab to go back to a field they skipped. Focus moves to it. The page scrolls so the field is in view. And the field is nowhere, because the page has a sticky header and the field is behind it. They are focused on something they cannot see.
Why it happens
When focus moves to an element outside the visible area, the browser scrolls so that the element is inside it. Scrolling the minimum distance is a reasonable default, and it lands the element at the very edge of the viewport or the scrolling container. If the element was above, it lands at the top edge. A sticky or fixed header sits on top of that edge. So the element lands exactly where the header is.
The same thing happens at the bottom with a sticky footer, a cookie bar, or a chat widget, and happens to any focused element: a link in text, a button, a tab. It only shows up when focus arrives by keyboard, because a pointer user has already scrolled the target into the open before they click it. That is why it is easy to ship.
The criterion
Success criterion 2.4.11, Focus Not Obscured (Minimum), is new in version 2.2 of the guidelines. It says that when an element receives focus, it is not entirely hidden by content the author made. "Entirely" is the AA line. The AAA version, 2.4.12, says not hidden at all. A field half under a header passes the minimum and fails the enhanced level. A field completely under it fails both.
The criterion is about content the author put there. A browser's own toolbar does not count. A sticky header does.
The fix is one line
Tell the browser where the real top of the scrolling area is:
html {
scroll-padding-top: 64px; /* the header's height, plus a little */
}
scroll-padding sets an inset on the scrollport. Every scroll the browser makes to bring something into view, whether for focus, for a fragment link, or for scrollIntoView(), respects it. With scroll-padding-top set to the header's height, "just far enough" now means "just far enough to clear the header". Put it on html for a page that scrolls, or on the scrolling container when the header is inside a panel. If the header changes height on narrow screens, change the padding with it.
The property has been in every major browser engine for years. It costs nothing, and it fixes fragment links at the same time, which have the same problem for the same reason.
What does not fix it:
scroll-marginon each field. It works, but it has to be on every element that can receive focus, including the ones added later.- A script that scrolls after focus. It fights the browser and flickers.
- Making the header not sticky on focus. Visitors notice.
How to test
Scroll to the bottom of a page with a sticky header. Press Shift+Tab until focus moves to something above the visible area. Look at where it landed. If you have to scroll to find it, the page fails.
An agent can do this with a scripted browser. Focus each stop in turn and read its box. Read the box of every element with position: sticky or position: fixed. Report any stop whose box is inside one of them. That is rule AW-FOCUS-004 in the ruleset, added with this note, and corpus page 34 plants the case.
Try it
The Tab Walk lab has a scrolling frame with a 56 px sticky header and eight fields. Press the button under it: the frame scrolls to the bottom and focus moves back to field 3, and the readout says what fraction of the field is under the header. Tick the scroll-padding-top fix and press again.
Sources
- w3.org, Understanding Success Criterion 2.4.11: Focus Not Obscured (Minimum)
- w3.org, Understanding Success Criterion 2.4.12: Focus Not Obscured (Enhanced)
- developer.mozilla.org, scroll-padding