One Tab Stop per Composite Widget
MUST: A composite widget (toolbar, listbox, menu, radiogroup, tablist, grid, tree) is ONE tab stop: roving `tabindex` — exactly one child at `0`, every other child at `-1` — with arrow keys moving the `0` and calling `.focus()`, and Tab moving past the whole widget. Twelve children must not be twelve tab stops. (Distinct from positive `tabindex`: here every value is `0` or `-1`; the bug is how many are `0`.)
Give a toolbar, listbox or grid a single tab stop and move within it using the arrow keys
<button tabIndex={i === activeIndex ? 0 : -1} onKeyDown={onArrows} />
// ArrowRight: setActiveIndex(i + 1); itemRefs[i + 1].current.focus();Bad
Good
Why it matters
Tab crosses widgets; arrow keys move within one. A toolbar, listbox, menu, radiogroup, tablist, grid or tree is ONE thing in the tab sequence, no matter how many children it has — that is what makes it a composite. Get this wrong and a twelve-item filter bar becomes twelve stops the user must Tab past to reach the form below, and the arrow keys, which is what a screen reader user will actually press inside a listbox, do nothing at all.
There are two implementations, and this one is roving tabindex: exactly one child carries tabindex="0" while every other child carries tabindex="-1", so the browser can only Tab into the active child; your arrow-key handler then moves the 0 to the next child and calls .focus() on it. Because real DOM focus moves, the browser scrolls the active child into view and paints the focus ring for free.
The alternative, aria-activedescendant, is a separate principle — reach for it only when focus must stay in a text input. Note this is not the same complaint as "Never Use Positive tabindex", which is about tabindex="1" and up wrecking the global document order; here every value is 0 or -1 and the bug is how many of them are 0.