Roving Tabindex: Keyboard Accessibility for Composite Widgets
Roving tabindex is an accessibility pattern that keeps a composite widget represented by a single stop in the browser's Tab sequence. Users navigate between its items with arrow keys, while JavaScript updates focus and tabindex so keyboard interaction remains predictable.
Roving tabindex is commonly used in composite interface components such as toolbars, tab lists, radio groups, menus, and grid-like widgets. Instead of requiring users to press Tab repeatedly through every child, the pattern provides one entry point into the component and uses directional keys for internal navigation.
Each item receives a tabindex value of either 0 or -1. The currently active item has tabindex="0", making it reachable through normal sequential keyboard navigation. All other items have tabindex="-1", which removes them from the sequential Tab order without preventing programmatic focus.
When the user presses an arrow key, the component identifies the next appropriate item, updates the old item's tabindex to -1, assigns tabindex="0" to the new item, and calls focus() on it. This keeps the DOM focus location synchronized with the component's active item. The implementation should also define behavior for boundaries, disabled items, and any orientation-specific navigation rules.
When focus leaves the widget, the active item generally retains tabindex="0". Consequently, pressing Tab again moves to the next focusable element outside the component, rather than visiting every internal item. When the user later tabs back into the widget, the browser can return focus to the retained entry point.
Roving tabindex is distinct from aria-activedescendant. Both patterns support composite widgets with managed internal navigation, but roving tabindex moves actual DOM focus between items. The alternative keeps DOM focus on a container and communicates the active descendant through an ARIA attribute. The choice affects event handling, focus styling, and how assistive technologies perceive the interaction.
Key points
- Use tabindex="0" for the current keyboard entry point and tabindex="-1" for other managed items.
- Handle arrow-key navigation within the composite widget.
- Update tabindex values and move actual DOM focus together.
- Preserve one sequential Tab stop for the entire widget.
- Define wrapping, orientation, disabled-item, and initial-focus behavior explicitly.
- Use native semantic elements and appropriate ARIA roles when building custom widgets.
Interview questions
What problem does roving tabindex solve, and how does it work?
It prevents a composite widget with many children from creating an excessive number of sequential Tab stops. One item has tabindex="0", while the others use tabindex="-1". Arrow-key handlers select another item, update those values, and move DOM focus to the selected element. Pressing Tab then leaves the widget through the normal document navigation sequence.
What is the difference between roving tabindex and aria-activedescendant?
Both approaches allow a composite widget to manage its active item without exposing every child as a separate Tab stop. Roving tabindex moves actual DOM focus between the child elements. With aria-activedescendant, focus remains on the container, which references the active child using an ID. Roving tabindex therefore requires coordinated focus and tabindex updates, while the alternative requires correct ARIA ownership and active-descendant references.
How would you implement arrow-key navigation in a toolbar?
I would maintain a reference to the toolbar items and an index identifying the active item. The keydown handler would recognize the supported directional keys, prevent their default behavior when the widget handles them, and calculate the next eligible item. I would then update the previous and next tabindex values and call focus() on the new item. I would also specify whether navigation wraps and how disabled items are handled.
How should a roving tabindex widget behave when focus leaves and later re-enters?
The last active item generally retains tabindex="0" when focus moves outside the widget. The other items remain at tabindex="-1", so they do not become additional sequential stops. When the user tabs back into the widget, the retained entry item is available as the sequential focus target. This behavior avoids resetting the user's position unnecessarily.
What accessibility and implementation edge cases would you test before shipping a custom composite widget?
I would verify that the widget has an appropriate semantic role, an accessible name where required, and exactly one sequential Tab stop. I would test keyboard navigation at boundaries, disabled items, orientation changes, and initial focus. I would also check that focus indicators remain visible and that the active item is communicated correctly to assistive technology. Finally, I would test actual browser and screen-reader combinations rather than assuming that correct tabindex values alone guarantee a usable experience.