No Login Data Private Local Save

Roving Tabindex Keyboard Nav - Online Accessible List

8
0
0
0

Roving Tabindex Keyboard Navigation

An accessible list implementing the roving tabindex pattern — use Tab to enter, ↑ ↓ to navigate, and observe how only one item holds tabindex="0" at a time.

Skip to the roving tabindex list

No items in the list

Add an item using the input below or click "Add Item".

Keys: Tab enter list, ↑ ↓ navigate, Home first, End last, Shift+Tab exit
Live Status
Total Items 0
Current Focus Index —
Item with tabindex="0" —
Last Action —
What is Roving Tabindex?

A technique where only one element in a group has tabindex="0" (making it the single Tab stop), while all others have tabindex="-1". Arrow keys move focus and update tabindex dynamically.

Benefits: Reduces Tab stops for keyboard users, simplifies navigation, and meets WCAG 2.1 AA requirements for keyboard accessibility.

/** * Roving Tabindex Pattern — Core Logic * Manages focus within a list so only one item is in the Tab order. */ class RovingTabindex { constructor(listEl) { this.list = listEl; this.items = () => this.list.querySelectorAll('[role="option"]'); this.currentIndex = 0; this.bindEvents(); this.updateTabindex(); } bindEvents() { this.list.addEventListener('keydown', (e) => this.handleKey(e)); this.list.addEventListener('click', (e) => { const item = e.target.closest('[role="option"]'); if (item) this.focusItem(item); }); } handleKey(e) { const items = this.items(); if (!items.length) return; const idx = this.currentIndex; let next = idx; switch (e.key) { case 'ArrowDown': next = Math.min(idx + 1, items.length - 1); break; case 'ArrowUp': next = Math.max(idx - 1, 0); break; case 'Home': next = 0; break; case 'End': next = items.length - 1; break; default: return; } e.preventDefault(); if (next !== idx) this.focusItemByIndex(next); } focusItem(item) { const items = this.items(); this.currentIndex = Array.from(items).indexOf(item); this.updateTabindex(); item.focus(); } focusItemByIndex(index) { const items = this.items(); if (index >= 0 && index < items.length) { this.focusItem(items[index]); } } updateTabindex() { const items = this.items(); items.forEach((item, i) => { item.setAttribute('tabindex', i === this.currentIndex ? '0' : '-1'); }); } }

Frequently Asked Questions

Roving tabindex is an accessibility pattern where only one element in a group (such as a list, toolbar, or grid) has tabindex="0" at any given time, making it the single Tab stop for keyboard users. All other elements have tabindex="-1", removing them from the natural Tab order. Users navigate between elements using arrow keys, and the tabindex "roves" to track the currently active element. This pattern is recommended by the WAI-ARIA Authoring Practices and helps reduce excessive Tab stops while maintaining full keyboard operability.

Use roving tabindex when you have a group of related, navigable items — such as a listbox, toolbar, menu bar, tab list, or grid — where users need to move focus among items efficiently. Standard tabindex="0" on every item forces keyboard users to Tab through each one individually, which becomes tedious with many items. Roving tabindex condenses the group into a single Tab stop, with arrow-key navigation inside the group, significantly improving the keyboard user experience. It is especially valuable for components with dynamic or large numbers of items.

Yes, roving tabindex works excellently with screen readers when implemented correctly. Screen readers follow focus, so when arrow keys move focus to a new item, the screen reader announces it. Pair roving tabindex with appropriate ARIA roles (like role="listbox" with role="option", or role="toolbar") and ensure each item has an accessible name. Also provide a live region announcement or aria-activedescendant where applicable. Testing with NVDA, JAWS, and VoiceOver is recommended to verify the experience.

Both patterns manage focus in composite widgets, but they differ fundamentally. Roving tabindex moves actual DOM focus between elements using element.focus() — each item receives real focus as the user navigates. Aria-activedescendant keeps focus on a single container element and uses the aria-activedescendant attribute to point to the "active" child's ID without moving DOM focus. Roving tabindex is simpler to implement and provides clearer focus visibility, while aria-activedescendant avoids focus flickering and can be more suitable for complex widgets like autocomplete listboxes. Choose based on your specific UX and accessibility requirements.

Absolutely. Roving tabindex extends naturally to 2D grids. In a grid, use Arrow Up/Down for vertical movement, Arrow Left/Right for horizontal movement, and Home/End for row boundaries. Add Ctrl+Home and Ctrl+End to jump to the first and last cells. The WAI-ARIA Grid Pattern is a well-established specification combining roving tabindex with role="grid", role="row", and role="gridcell". This pattern is used in spreadsheet-like interfaces, data tables with interactive cells, and calendar widgets.

When an item is added, insert it into the DOM and decide whether to move focus to it (common for "add new" actions) or keep focus where it is. Recalculate indices and update tabindex accordingly. When an item is deleted, if it currently has focus, move focus to the next sibling (or previous if it's the last item). Always call your updateTabindex() method after DOM changes to keep the tabindex state consistent. If the list becomes empty, ensure the container or a fallback element can receive focus gracefully.

Roving tabindex is not explicitly required by WCAG, but it helps meet several success criteria. WCAG 2.1.1 (Keyboard) requires all functionality to be operable via keyboard. WCAG 2.4.3 (Focus Order) requires focus to move in a logical order. WCAG 2.4.7 (Focus Visible) requires visible focus indicators. Roving tabindex provides an efficient, logical navigation pattern that satisfies these criteria for composite widgets. While you could technically meet WCAG without it (e.g., by making every item a Tab stop), roving tabindex delivers a far superior keyboard experience, which aligns with WCAG's underlying goal of accessibility.