Keyboard Access to Scroll-Driven Interfaces

Scroll-driven effects are driven by scrolling, and scrolling is something keyboards do perfectly well β€” provided the thing being scrolled is reachable. The failures in this area are rarely about the animation and almost always about the container: a panel that only scrolls with a pointer, a gallery with no accessible name, a header that hides while its links stay focusable. This guide covers each. It sits under accessible transitions.

When to use this approach

  • Any component with its own scroll container. The document scrolls with the keyboard by default; a nested container does not until it can receive focus.
  • Horizontal galleries and carousels. These are the most common place for content to be entirely unreachable without a pointer.
  • Sticky headers that hide. Anything moved off-screen with a transform keeps its focusable children in the tab order.
  • Pinned or stacked sections. The visual stacking must not imply that covered content is unreachable, because it is not.
What needs explicit keyboard work Four component shapes. A page-level reveal needs nothing, because the document already scrolls with the keyboard. A nested scroll container needs a tabindex to be scrollable at all. A horizontal gallery needs that plus a name. A pinned stack needs its covered content to stay reachable in order. Page-level reveals β€” nothing to do; the document scrolls with the keyboard already Nested scroll container β€” needs tabindex="0" or its content is unreachable Horizontal gallery β€” tabindex plus an accessible name, or it is an unlabelled region Pinned stack β€” covered cards must stay reachable in document order

Implementation

1. Make nested scroll containers focusable

<div class="log" tabindex="0" role="region" aria-label="Deployment log">
  …
</div>

A scrollable element with tabindex="0" can be focused and then scrolled with the arrow keys, Page Up and Page Down. Without it, a keyboard user has no way to reach content below the fold of that container β€” the page scrolls past it instead.

Browsers increasingly make scrollable regions keyboard-focusable automatically, but the behaviour is not universal across engines and versions, so the explicit attribute is still the reliable form.

2. Give the region a name

An unlabelled focusable region announces as β€œregion” and nothing else. A short aria-label β€” or an aria-labelledby pointing at a nearby heading β€” tells the reader what they have arrived at before they start scrolling it.

3. Expect focus to drive the timeline

Focus scrolls, and scrolling animates Tabbing to an element outside the visible part of a scroll container makes the browser scroll it into view. That scroll drives any timeline bound to the container, so the animation advances. This is correct behaviour and worth testing deliberately, because it is a code path a pointer never exercises. scroll container visible visible off-screen β€” Tab lands here The browser scrolls it into view, which advances any timeline bound to this container. The animation is being driven by the keyboard β€” a path a pointer never exercises. Tab through every animated section once. It is the fastest way to find effects that only work with a mouse.

This is correct behaviour rather than something to prevent: the reader is moving through the content and the animation is following. What it does mean is that keyboard navigation is a genuine input path for your effects, and one that is never exercised by pointer testing.

/* make the movement legible rather than instantaneous */
.gallery { scroll-behavior: smooth; }

@media (prefers-reduced-motion: reduce) {
  .gallery { scroll-behavior: auto; }
}

4. Stop hidden headers trapping focus

The hidden focusable trap A header hidden by translating it off-screen keeps its links focusable. A keyboard user tabbing into them moves focus to something invisible, with no indication of where they are. Either remove the links from the tab order while hidden, or bring the header back when it receives focus. header translated off-screen still in the tab order focus goes somewhere invisible returns on focus-within the header slides back when a link inside it receives focus .site-header:focus-within {{ translate: 0 0; }} One declaration, and the pattern stops being a trap.
.site-header {
  transition: translate 200ms ease;
}
.site-header.is-hidden { translate: 0 -100%; }
.site-header.is-hidden:focus-within { translate: 0 0; }

Bringing the header back when anything inside it receives focus is one declaration and removes the trap entirely. The alternative β€” removing the links from the tab order while hidden β€” also works and is more code.

5. Keep pinned content in document order

A stacked-card section produces its visual order through sticky positioning and transforms, neither of which changes the DOM. Tab order therefore follows the markup, which is what you want β€” but it is worth confirming, because a implementation that reorders cards with order or absolute positioning will not.

Verification

Unplug the mouse, metaphorically or otherwise, and use each animated section. This single exercise finds more than any checklist: a container you cannot scroll, a gallery whose later items are unreachable, a header that swallows focus.

Tab from the element before each animated section to the element after it, watching the focus ring the whole way. Every stop should be visible; a stop where nothing appears to have focus is either a hidden focusable element or a focus style that has been removed.

Scroll a nested container with the arrow keys after focusing it, and confirm any timeline bound to it advances as expected. If the animation only responds to the wheel, the container is being scrolled by the page rather than by itself.

Finally, check the reduced-motion path. Smooth scroll behaviour is motion, and a reader who has asked for less of it should get instant scroll positioning rather than a glide.

Edge cases and gotchas

tabindex="0" on a container with focusable children. The container itself becomes a tab stop before its children, which adds one stop. That is acceptable and expected for a scrollable region; it becomes annoying when applied to containers that are not scrollable, so apply it only where it is needed.

Removed focus styles. A design that suppresses outlines globally makes every keyboard path invisible. :focus-visible gives the control most designs actually want without removing the affordance.

Scroll-snap and keyboard scrolling. Arrow-key scrolling in a mandatory-snap container jumps a whole snap point at a time, which can make fine positioning impossible. Proximity snapping restores the ability to scroll by increments.

Focus inside a content-visibility: auto section. Focusing an element in a skipped section forces it to render, which is correct β€” but any animation in that section starts from whatever progress the scroll position implies, which can look like a jump.

Sticky headers overlapping the focused element. A focused element scrolled to the top of the viewport can end up underneath a sticky header. scroll-margin-block-start on focusable elements, set to the header height, fixes it.

Browser-specific notes

Keyboard scrolling of a focused container behaves consistently across engines. Where they differ is in whether a scrollable container is focusable without an explicit tabindex.

Chromium has shipped automatic focusability for keyboard-scrollable regions that contain no focusable children, which covers the common case of a scrollable log or code block. Firefox has similar behaviour. Safari is the least consistent of the three, which is the practical reason to keep writing the attribute explicitly rather than relying on it.

The accessibility checkers built into each engine’s developer tools all flag a scrollable region without a name, and all three are worth running once per component β€” this is one of the rare accessibility issues that automated tooling catches reliably.

Frequently Asked Questions

Does adding tabindex="0" hurt anything?

It adds one tab stop per container, which is a small cost against making the content reachable at all. The mistake to avoid is applying it to elements that are not scrollable and not interactive, where it adds stops that do nothing. Scrollable regions are exactly the case it exists for.

If the scrollbar is hidden, yes. A horizontally scrollable region with no visible scrollbar and no controls is discoverable only by trying, which is a poor experience for everyone and an impossible one for some. Buttons also give keyboard users a way to move by item rather than by pixel.

How does focus interact with a pinned stack?

Focus moves through the cards in document order and the browser scrolls each into view, which unpins the previous card and pins the next β€” exactly as scrolling would. The thing to verify is that focusing a card that is currently covered brings it forward rather than leaving focus on something the reader cannot see.

Is scroll-behavior: smooth an accessibility problem?

It is motion, so it belongs under the reduced-motion preference like any other. It is generally helpful otherwise, because an instantaneous jump on focus can be more disorienting than a short glide β€” which is why the recommendation is to use it and override it, rather than to avoid it.

What about a pinned section that consumes several viewports?

Keyboard scrolling moves in fixed increments, so a section designed around several viewports of scroll takes a great many key presses to get past. That is a usability problem rather than a correctness one, and it lands hardest on the people least able to work around it.

The mitigation is the same one that helps everyone: shorter per-step distances, so the section is passable at a reasonable rate. Where the section genuinely must be long, a skip link that jumps to the content after it is a small addition that changes the experience considerably.


Up: Accessible Transitions: Focus & Announcements