Scrollable areas need keyboard access
- Rule:
axe/scrollable-region-focusable - Checks: Scrollable region must have keyboard access
- WCAG: 2.1.1, 2.1.3
- Fix class:
mechanical: A fix needs no authored text or visual decision.
Who it affects
Section titled “Who it affects”- People who use a keyboard or a switch device instead of a mouse.
How to fix it
Section titled “How to fix it”A scrollable box holds only text, images or a table
Section titled “A scrollable box holds only text, images or a table”Add tabindex="0" so keyboard users can focus the box and scroll it with the arrow keys. Give it role="region", a name with aria-label or aria-labelledby, and a visible focus style.
<div class="terms" tabindex="0" role="region" aria-labelledby="terms-title"> <h2 id="terms-title">Terms of service</h2> <p>By creating an account you agree to these terms.</p></div>The box contains only elements with tabindex="-1"
Section titled “The box contains only elements with tabindex="-1"”axe asks for review. If script moves focus through those elements, such as with the arrow keys in a listbox, check that keyboard users can reach all the content. If nothing ever focuses them, make the box focusable as above.
The box scrolls only because content overflows its fixed size
Section titled “The box scrolls only because content overflows its fixed size”If scrolling is not intended, remove the fixed height or the overflow setting so the content shows in full. The box then needs no focus.
Fixes that fail
Section titled “Fixes that fail”Relying on the browser to make the box focusable
Section titled “Relying on the browser to make the box focusable”Some browsers make scroll containers focusable on their own, but Safari does not, and none gives the box a name.
Adding tabindex="0" without a role or a name
Section titled “Adding tabindex="0" without a role or a name”Keyboard users can scroll, but screen readers announce nothing useful when the box takes focus, and axe reports it under focus-order-semantics.
In React
Section titled “In React”The part of the fix that differs in React, in its syntax. The rest of the guidance holds.
A reusable scroll container names itself with aria-labelledby
Section titled “A reusable scroll container names itself with aria-labelledby”Make the heading’s id with useId, so each instance in a React root points at its own heading, and pass tabIndex={0} and role="region" to the element that scrolls. If the page holds more than one root, give each its own identifierPrefix.
function ScrollPanel({ title, children }) { const titleId = useId();
return ( <div className="scroll-panel" tabIndex={0} role="region" aria-labelledby={titleId}> <h2 id={titleId}>{title}</h2> {children} </div> );}A scroll-area component scrolls an inner viewport element, not its outer wrapper
Section titled “A scroll-area component scrolls an inner viewport element, not its outer wrapper”Put tabIndex, role and the name on the inner element that scrolls, through the prop or ref the component offers for its viewport.
Fixes that fail in React
Section titled “Fixes that fail in React”Hard-coding the heading id in the component
Section titled “Hard-coding the heading id in the component”Two instances on one page then share the id, and both regions take the first heading’s name.
More on the rule
Section titled “More on the rule”axe-core’s documentation of the rule
CORA’s fix guidance adapts content from accessibility-agents, under the MIT License: see its notice.