Roving Tabindex — Single Tab Stop for Widget Groups
In this tutorial, you will learn about Roving Tabindex. We cover key concepts, practical examples, and best practices to help you master this topic.
Roving tabindex is a pattern where only one element in a widget group has tabindex=0 while others have tabindex=-1, with arrow keys shifting focus and updating tabindex values.
In this tutorial, you'll learn to implement roving tabindex for Keyboard Navigation.
What You'll Learn
By the end of this lesson, you'll understand the roving tabindex pattern, how to implement it for tabs, menus, and radio groups, and how it compares to aria-activedescendant.
Why It Matters
Without roving tabindex, users must press Tab multiple times to navigate within a widget. Roving tabindex makes complex widgets efficient.
Real-World Use
Doda Browser uses roving tabindex in the tab panel, toolbar, and menu components.
Roving Tabindex Flow
flowchart LR A[Widget group] --> B[One item tabindex='0'] A --> C[Other items tabindex='-1'] D[User presses ArrowRight] --> E[Remove tabindex from current] E --> F[Set tabindex='0' on next item] F --> G[Focus next item] G --> H[User presses Tab] H --> I[Focus leaves widget at current item]
Implementing Roving Tabindex
<div role="tablist" aria-label="Scan options">
<button role="tab" aria-selected="true" tabindex="0">Quick</button>
<button role="tab" aria-selected="false" tabindex="-1">Full</button>
<button role="tab" aria-selected="false" tabindex="-1">Custom</button>
</div>
function handleArrowKey(event, container) {
const items = Array.from(container.querySelectorAll('[role="tab"]'));
const currentIndex = items.findIndex(el => el === document.activeElement);
let newIndex;
if (event.key === 'ArrowRight') {
newIndex = (currentIndex + 1) % items.length;
} else if (event.key === 'ArrowLeft') {
newIndex = (currentIndex - 1 + items.length) % items.length;
} else {
return;
}
event.preventDefault();
items.forEach((item, i) => {
item.setAttribute('tabindex', i === newIndex ? '0' : '-1');
});
items[newIndex].focus();
}
The Tab Test
Tab should enter the widget at the currently active item and leave the widget from the same item. This is the key behavior of roving tabindex.
Roving Tabindex vs aria-activedescendant
| Pattern | Focus | Tab stop | Best for |
|---|---|---|---|
| Roving tabindex | Moves between items | Single tab stop per item | Widgets with few items |
| aria-activedescendant | Stays on container | Single tab stop on container | Widgets with many items or text input |
Common Mistakes
- Not resetting tabindex when selection changes: Stale tabindex values create multiple tab stops.
- Using roving tabindex for very large lists: aria-activedescendant is better for hundreds of items.
- Not handling Home and End keys: Users expect to jump to start and end.
- Forgetting to manage tabindex on dynamic items: Added or removed items need tabindex updates.
- Using roving tabindex for single-select without managing aria-selected: Selection state must match tabindex.
Practice and Challenge
1. What is roving tabindex? A pattern where only one item in a group is in the tab order.
2. What keys typically navigate within a roving tabindex widget? Arrow keys.
3. Where does focus go when Tab is pressed on a roving tabindex widget? To the next focusable element outside the widget, starting from the currently active item.
4. When should you use aria-activedescendant instead? When the widget has many items or includes a text input.
5. Challenge: Implement a radio group with roving tabindex. Only the selected radio should be in the tab order.
FAQ
Mini Project
Build a toolbar with three buttons using roving tabindex. Arrow keys navigate between buttons, Tab enters and exits. Verify with keyboard testing.
What's Next
Continue to Arrow Key Navigation to learn about implementing arrow key interactions.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro