Affix

Pins an element to the viewport once the page scrolls past it, and puts a placeholder of the same size in its place so nothing below jumps.

Accessibility

Affix has no role, no label and no keyboard behavior of its own. It takes an element you wrote and makes it position: fixed. The sections below are about what that does to the page underneath.

Affix adds no semantics

Affix renders two plain <div>s and gives them no role, no label and no state. It moves its child and nothing else, so it contributes nothing to the accessibility tree and there is nothing on it to configure. Whatever you pin is as accessible as you made it.
  • The only ARIA it adds is aria-hidden="true" on the placeholder it leaves in the page. The placeholder is an empty box holding the child's space, so there is nothing to read.
  • Extra attributes go on the outer div, not on the pinned one. A role, an aria-label or an id passed to Affix names the element that stays in the page, not the one the reader sees pinned. Put them on the child.
  • A pinned toolbar or navigation bar still needs its own landmark and its own name. Wrapping it in Affix does not give it one, and does not take one away either.

A pinned element covers content

A pinned bar takes its height out of the readable viewport for the whole document. Two readers feel it most, and neither is on the desktop the bar was designed on.
  • The reader who has zoomed in. At 200% (WCAG 1.4.4 Resize Text) the bar is twice as tall as well, so a 64px header is eating 128 CSS pixels of a viewport that has not grown. Pin as little as the screen can do without.
  • The reader on a short viewport. A phone in landscape, a split window, a browser with three toolbars open. Keep the total pinned height well under a third of the shortest viewport you support.
  • Prefer a bar that gets shorter once it pins, driven by onChange, to one that is tall the whole way down. Call updatePosition() after it changes height, or the placeholder holds the old one.
  • An offsetBottom bar stacks with the browser's own chrome and with the on-screen keyboard. A bottom-pinned action bar can end up underneath the keyboard on a phone, where it is neither visible nor reachable.

Reading order follows the DOM

Affix does not move the child in the DOM. It renders the fixed element in place and puts the placeholder beside it, so a screen reader and a keyboard both reach the pinned element where it was written, whatever its visual position.
That is usually what you want, and it is also the mismatch to design around. Something painted at the top of the screen but written halfway down the document is read halfway down. A sighted reader sees it first, a screen reader user meets it last.
  • Put the element you pin where it makes sense to read it, near the top of the section it belongs to, so the two orders stay close.
  • Do not correct the mismatch with a positive tabindex on the child. A positive value reorders the entire page's tab sequence, not this one element, and the page is worse afterwards.
  • A bar that is pinned for the whole document is content a keyboard reader passes every time they return to the top. If it holds more than a few controls, give it a skip link the way a site header gets one.

Focus landing behind the pinned element

A keyboard reader hits this and a mouse reader never sees it. Tabbing moves focus down the page, the browser scrolls the focused control to the edge of the viewport, and the pinned bar is drawn over that edge. The control has focus and cannot be seen.
  • The fix is scroll-margin-top on the focusable content, or scroll-padding-top on the scroll container to solve it once for everything inside. Match the value to the pinned height. For an Anchor, pass the same value as targetOffset.
  • An offsetBottom bar needs the mirror, scroll-margin-bottom, which is the one people forget.
  • Keep the focus ring on everything inside the pinned element. It floats over unrelated content, so the surrounding text does not tell the reader where they are.
  • Test it by tabbing, not by clicking. None of these show up with a pointer.

Reflow at 320 pixels

WCAG 1.4.10 Reflow asks that content work at 320 CSS pixels wide and 256 tall without a second scroll direction. Affix adds no horizontal scrolling itself, but a pinned bar at that size is usually too tall.
  • A toolbar that is one row at desktop width becomes two or three at 320px, so the pinned height doubles or triples against a viewport that is now 256 pixels tall. At that point the bar is the page.
  • If the pinned thing does not fit, do not pin it at that width. offsetTop, offsetBottom and target are ordinary values, so render the child without Affix below a breakpoint in your own code.
  • Check it at that size. The placeholder takes the child's size, so a bar that wraps to three rows also reserves three rows of space in the page.