web3icons

Focus not obscured: sticky headers and WCAG 2.4.11

How sticky headers, banners and bottom sheets hide focused elements, what WCAG 2.4.11 and 2.4.12 require, and fixes such as scroll-padding and inert.

5 min read

Picture a markets page with a sticky site header and a sticky column header on the token table. A keyboard user presses Shift+Tab to move back up the list. The browser sees that the focused row is inside the viewport, so it doesn't scroll, but the row is sitting under 120 pixels of sticky header. Focus is on something nobody can see.

WCAG 2.2 added two criteria for exactly this. This guide explains what it requires, where the problem shows up in crypto interfaces, and how to fix it with a few lines of CSS.

What the criteria say

2.4.11 Focus Not Obscured (Minimum), Level AA. When a component receives keyboard focus, it is not entirely hidden by author-created content.

2.4.12 Focus Not Obscured (Enhanced), Level AAA. When a component receives keyboard focus, no part of it is hidden by author-created content.

So at AA, a focused button that's half under a sticky header passes, and one that's completely under it fails. At AAA, any overlap fails. Partly hidden is still a poor experience, especially when the visible part is the edge of a row with no ring, so the fixes below aim for fully visible.

Two notes in the criterion matter in practice. If the user can move content around, as with a draggable panel, only its initial position is considered. And content the user opened, such as an expanded menu, doesn't count as obscuring focus if the user can reveal the focused element without moving focus, for example by pressing Escape to close it.

"Author-created" means content you put on the page. A browser's own UI, such as an autofill dropdown, isn't your responsibility here.

Where it happens in crypto interfaces

  • Sticky site headers, often tall on trading pages because they hold the logo, navigation, network button and wallet button.
  • Sticky table headers in markets lists, portfolio tables and transaction histories, stacked under the site header.
  • Sticky toolbars and filter bars above token grids and NFT galleries.
  • Cookie and consent banners fixed to the bottom of the viewport, sometimes a third of its height on phones.
  • Toasts such as "Transaction submitted", stacked in a corner over the content.
  • Persistent bottom bars on mobile web: tab bars, "Connect wallet" bars, pending-transaction drawers.
  • Chat and support widgets whose launcher sits on top of the last row of a list.

Direction matters. Moving forward with Tab, things disappear under bottom bars and banners. Moving backward with Shift+Tab, they disappear under sticky headers. Test both.

Fix 1: scroll-padding

scroll-padding tells the browser which part of a scroll container is actually visible. When it scrolls an element into view, it keeps the element out of the padded area. Set it to the height of whatever is stuck to each edge:

html {
  scroll-padding-top: calc(var(--header-height, 64px) + 8px);
  scroll-padding-bottom: calc(var(--bottom-bar-height, 0px) + 8px);
}

The extra 8px leaves room for the focus ring. The same padding also fixes in-page anchor links landing under the header.

Whether a browser also applies scroll padding when keyboard focus lands on a covered element has varied between engines and versions, so verify it by tabbing through the page in each browser you support. If one doesn't scroll, a small script can close the gap by scrolling the overlap away when focus arrives:

const header = document.querySelector('.site-header')

document.addEventListener('focusin', (event) => {
  if (header.contains(event.target)) return
  const overlap = header.getBoundingClientRect().bottom - event.target.getBoundingClientRect().top
  if (overlap > 0) window.scrollBy(0, -(overlap + 8))
})

Scroll padding belongs on the element that scrolls. If a token list scrolls inside a panel with its own sticky column header, set it there:

.token-list {
  overflow-y: auto;
  scroll-padding-top: 44px;
}

.token-list thead th {
  position: sticky;
  top: 0;
}

Fix 2: measure what's stuck

Hard-coded heights drift. The header grows when a "You're on a testnet" notice appears, when the network name wraps at 200% zoom, or when the wallet button switches from "Connect" to an ENS name and avatar. Measuring keeps the padding honest:

const header = document.querySelector('.site-header')
const root = document.documentElement

const update = () => {
  root.style.setProperty('--header-height', `${header.offsetHeight}px`)
}

new ResizeObserver(update).observe(header)
update()

Do the same for any bottom bar, and set its variable to 0px when the bar is hidden.

Fix 3: give overlays their own space

Some overlays are better fixed by layout than by scrolling.

Consent banners. A banner placed in the normal document flow pushes content instead of covering it. If it must be fixed, feed its height into --bottom-bar-height while it's visible.

Toasts. Keep them small, put them in a corner that doesn't overlap primary actions (a toast over the "Confirm swap" button is the worst spot), and let them be dismissed. The content of a toast also needs to reach screen readers; announcing transaction status covers that side.

Persistent bottom bars. Add bottom padding to the page equal to the bar's height, so the last items in the page can scroll above it, and set scroll-padding-bottom to match. Mobile wallet UI patterns discusses these bars in more detail.

Modals and bottom sheets

A modal dialog or modal bottom sheet, such as a swap review or a wallet picker, should keep focus inside itself while it's open. Then nothing behind it can receive focus, and nothing can be obscured. The native <dialog> element does this for you when opened with showModal(), which makes the rest of the page inert:

const dialog = document.querySelector('#review-swap')
dialog.showModal()

For a custom modal, set the inert attribute on the page content behind it while it's open. Both approaches also stop screen readers from wandering into the background.

Non-modal sheets are where trouble starts: a pending-transactions drawer that stays open while the page underneath remains usable. Treat it like a bottom bar. Reserve its height with padding and scroll-padding-bottom, or let people collapse it to a small handle.

Testing

  1. Tab forward through every page with sticky or fixed elements, then Shift+Tab all the way back.
  2. Repeat at a short viewport (around 720px tall on desktop), at 200% zoom, and at phone width.
  3. Repeat with the consent banner visible. Clear site data or use a private window so it appears.
  4. Trigger toasts and drawers, then keep tabbing while they're on screen.
  5. Watch for focus that is partly hidden. It may pass at AA, but if the visible sliver doesn't show the ring, fix it anyway.

Pair this with a strong ring from designing visible focus styles; a visible indicator and an unobscured element are two halves of the same requirement. The other WCAG 2.2 additions are summarized in what changed in WCAG 2.2.

More in Accessibility

All guides