# Sticky headers and banners that hide keyboard focus

Source: https://easeweb.dev/learn/focus-not-obscured
Topics: Keyboard and focus > Sticky headers do not hide the focused control (WCAG 2.4.11)
The fix: Keep the bars from covering what the browser scrolls to. Either set scroll-padding on the page to the height of the sticky header and the fixed banner, so focus stops clear of them, or lay the page out so only the main content scrolls and the bars sit outside it.
Test with: keyboard, zoom
References: https://www.w3.org/WAI/WCAG22/Understanding/focus-not-obscured-minimum.html https://www.w3.org/WAI/WCAG22/Techniques/css/C43 https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-padding

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A gardening site, Garden Notes, lists its articles as links: "Spring planting guide", "Choosing a compost bin", "Pruning roses" and more. The site header, with the logo and menu, sticks to the top of the window. A cookie banner is fixed to the bottom until someone answers it. Press Tab down the list: the next two links sit behind the cookie banner, and focus moves onto them without the page moving. Focus is on a link nobody can see. Press Shift + Tab back up and the same happens under the header.

## Why it fails

When focus moves, the browser scrolls only if the element is outside the scroll area. A link behind a sticky header or fixed banner is inside that area, so as far as the browser knows it is already in view, and the page does not move. When the browser does scroll, how far depends on the browser: some centre the element, others stop with it flush against the edge, under the bar. Either way, nothing tells the browser that the bars cover part of the window. Success criterion 2.4.11 requires that a focused control is not entirely hidden by content the author put there. Here it is entirely hidden, in both directions.

## Who is affected

Sighted keyboard users, including people who use switch access or voice control that moves focus, lose track of where they are and may press Enter on a link they never saw. People who zoom in are hit hardest: at 200% the header and banner can fill a third of the window, so more of the page sits under them.

## What automation and AI miss

The markup is correct: the links have names, the order is right, and a focus style is defined. Hidden focus only exists after a scroll, at a given window size, with a banner that may only show on a first visit. Most scanners load the page once at the top and never tab through it. A generated layout often adds `position: sticky` without anything that reserves its space.

## Before: bars with nothing reserving their space

```css
.site-header {
  position: sticky;
  top: 0;
  height: 4rem;
}
.cookie-banner {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  height: 5rem;
}
```

The page scrolls under both bars, and nothing tells the browser that the top 4rem and bottom 5rem of the window are covered. Every link it scrolls into view by focus stops under one of them.

## After: scroll-padding the height of the bars

```css
:root {
  --header-height: 4rem;
  --banner-height: 5rem;
}
.site-header {
  position: sticky;
  top: 0;
  height: var(--header-height);
}
.cookie-banner {
  position: fixed;
  inset-inline: 0;
  bottom: 0;
  height: var(--banner-height);
}
html {
  scroll-padding-top: var(--header-height);
  scroll-padding-bottom: var(--banner-height);
}
/* When the banner is answered and removed, give the space back. */
html:not(:has(.cookie-banner)) {
  scroll-padding-bottom: 0;
}
```

`scroll-padding` tells the browser which part of the window is really visible, so focus scrolling, anchor links and `scrollIntoView` stop clear of the bars. The heights and the padding come from the same tokens, so they cannot drift apart. This works only while the bars keep those heights: if the header's text wraps at a narrow width or when enlarged and the header grows, the padding must grow with it. Use `rem` heights that scale with the text, or update the variables from a `ResizeObserver`. Set `scroll-padding` on the element that actually scrolls; if the content scrolls inside a container, put it there instead of on `html`.

## After: only the content scrolls

```css
body {
  display: grid;
  grid-template-rows: auto 1fr auto;
  height: 100dvh;
  margin: 0;
}
.site-header,
.cookie-banner {
  position: static;
}
main {
  overflow-y: auto;
}
```

The header and banner are rows of the layout rather than layers over it. Focus scrolls inside `main`, whose visible area ends where the bars begin, so nothing can sit on top of the focused link. There are no heights to keep in step: a wrapping header just makes its row taller.

## Which option to use

Both pass. `scroll-padding` is the smaller change and keeps the normal page scroll, including the mobile address bar that hides as you scroll, but it depends on the reserved space matching the bars. The grid layout needs no measuring, but it moves scrolling from the page into `main`, which changes how pull-to-refresh, scroll restoration and some analytics scripts behave. Choose `scroll-padding` for an existing site where the bars have fixed heights; choose the layout when you are building the page shell or the bars change height with their content.

## Implementation decisions

Count every layer that floats over the page: the header, cookie and promotion banners, chat launchers and "back to top" buttons. Each one needs its space reserved or a place outside the scroll area. A floating button in a corner can hide a small control entirely even when the bars are handled.

A banner people can dismiss still fails while it is showing, so reserve its space until it is gone. When zoomed in, very tall bars can leave almost no room for content; consider letting the header scroll away at small heights, for example with a `@media (max-height: 30rem)` rule that turns `position: sticky` off. Partly covered focus passes 2.4.11 at level AA, but 2.4.12 at AAA asks for no covering at all, and partly hidden focus is still hard to read: aim for the whole control to show.

## Verify the fix

1. Scroll to the top, then press Tab through every control to the end of the page, with the header and cookie banner showing. Each focused control stays at least partly visible.
2. From the end, press Shift + Tab back to the top and check the same thing under the header.
3. Repeat at 200% zoom and in a 320-pixel-wide window, where the header may wrap onto two lines.
4. Repeat with each overlay state you ship: banner shown and dismissed, chat open and closed, any promotion bar.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the header, banners or page layout change.

## Limits

This is a starting pattern, not a guarantee for every application. Browsers apply `scroll-padding` to focus scrolling in their current versions, but check yours, and check scroll containers inside the page, such as carousels and side panels, which each need their own padding. Token names in the snippets are placeholders for your own design tokens.
