Lesson 8 of 30
A toolbar of 10 buttons shouldn't cost a keyboard user 10 Tab presses just to get past it. Roving tabindex is the pattern that makes a whole group of controls behave as one stop in the page's tab order.
13 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
Question 1 of 3
A toolbar of 8 buttons gives every button `tabindex="0"`. What's the practical problem with this, from a keyboard user's perspective?
Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
A composite widget is a group of related controls that behaves as a single unit — a toolbar of buttons, a tab list, a menu, a grid of cells. The naive way to make each control keyboard-reachable is to give every one of them tabindex="0", so each becomes its own stop in the page's tab order. That technically works, but it's a real usability problem: tabbing past a 10-item toolbar now takes 10 separate Tab presses, when a mouse user just glances at it and clicks whatever they want.
The fix real toolbars, tab lists, and menus use is roving tabindex: the group behaves as exactly ONE stop in the page's overall tab order, and arrow keys move focus BETWEEN the group's items once you're inside it. The mechanics:
tabindex="0".tabindex="-1" — focusable programmatically, invisible to Tab.tabindex="0" as the "active" one changes — the 0 value literally "roves" from item to item.This is exactly the mechanism underneath ARIA composite roles like role="toolbar", role="tablist", and role="menu". Those roles declare the widget's semantics to a screen reader — "this is a toolbar," "this is a set of tabs" — but roving tabindex is the actual keyboard-behavior implementation that makes the semantics true. Adding the ARIA role without the roving-tabindex behavior behind it produces a widget that's announced correctly but still doesn't behave like the thing it claims to be.
<!-- Correct: roving tabindex — ONE tab stop, arrow keys move within it -->
<div role="toolbar" aria-label="Text formatting">
<button tabindex="0">Bold</button>
<button tabindex="-1">Italic</button>
<button tabindex="-1">Underline</button>
<button tabindex="-1">Strikethrough</button>
</div>
toolbar.addEventListener("keydown", (e) => {
if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return;
const items = [...toolbar.querySelectorAll("button")];
const current = items.indexOf(document.activeElement);
const next = e.key === "ArrowRight"
? items[(current + 1) % items.length]
: items[(current - 1 + items.length) % items.length];
items[current].tabindex = -1; // the old active item steps back
next.tabindex = 0; // tabindex "roves" to the new active item
next.focus();
});
<!-- Wrong: every button is its own tab stop — 4 separate Tab presses required -->
<div role="toolbar" aria-label="Text formatting">
<button tabindex="0">Bold</button>
<button tabindex="0">Italic</button>
<button tabindex="0">Underline</button>
<button tabindex="0">Strikethrough</button>
</div>
Roving tabindex: only the active button has tabindex="0" — the whole toolbar is ONE stop in the page's tab order.
The role="toolbar" markup looks identical in both versions above — the difference that actually matters for keyboard usability is entirely in the tabindex values and the arrow-key handler, not in the ARIA attribute. A screen reader announces both as "toolbar," but only the roving-tabindex version behaves like one.
role="toolbar" tells a screen reader what the group is; it does nothing to change how Tab or arrow keys behave. Both have to be built.tabindex="0" at any given time — not zero, not more than one. Zero means the group is unreachable by Tab at all; more than one reintroduces the "too many tab stops" problem the pattern exists to solve.tabindex="0" needs to be reapplied to the same logical item, not silently reset to the first one every time."How would you make a custom toolbar or tab list keyboard-accessible without making users Tab through every single item?" is a real, specific question, and naming roving tabindex — the 0/-1 split and the arrow-key handler that moves it — is the concrete answer that separates "I'd add ARIA roles" from actually knowing the mechanism underneath them.