How does an application menu differ from a nav list? What is roving tabindex and why use it?
The composite-widget model: one tab stop, arrow-key navigation, and roving vs activedescendant.
First, a distinction interviewers probe: a site NAVIGATION (links to pages) is NOT an ARIA menu — it's a <nav> with a list of links, and you should not slap role="menu" on it. role="menu"/"menubar" is for an APPLICATION menu of commands/actions (like a desktop app's menu bar), with role="menuitem" (and menuitemcheckbox/menuitemradio) children. Getting that wrong makes screen readers announce 'menu' and impose command-menu key expectations on what are really links. The core interaction model for menus — and for tabs, listboxes, radio groups, tree grids, any COMPOSITE widget — is that the whole widget is a SINGLE tab stop: Tab moves INTO and OUT of it as one unit, and ARROW keys move between the items inside. That's the WAI-ARIA convention users expect. Two ways to implement 'arrows move the active item': (1) ROVING TABINDEX — exactly one item has tabindex="0" (the current one) and all others have tabindex="-1"; on arrow key you move real DOM focus to the new item and swap the tabindex values so it becomes the new tab stop. Real focus moves, so focus styles and getElementById-free focus work naturally. (2) aria-activedescendant — focus stays on the container and you point activedescendant at the active child's id (the combobox approach). Roving tabindex suits menus/toolbars/radio groups where items are real focusable elements; activedescendant suits inputs that must keep focus (comboboxes). Either way: Tab is one stop, arrows navigate, Home/End jump, Escape closes a popup menu and returns focus to its trigger, and typeahead (type a letter to jump) is expected in menus. The bug to avoid is making every item its own tab stop — that's the sign someone reconstructed a menu from plain buttons without the composite model.
Building menus, toolbars, tab sets, radio groups; choosing roving tabindex vs activedescendant.
// Roving tabindex: one item is the tab stop (0), the rest are -1.
function Menu({ items }) {
const [active, setActive] = React.useState(0);
const refs = React.useRef([]);
function onKeyDown(e) {
let next = active;
if (e.key === 'ArrowDown') next = (active + 1) % items.length;
else if (e.key === 'ArrowUp') next = (active - 1 + items.length) % items.length;
else if (e.key === 'Home') next = 0;
else if (e.key === 'End') next = items.length - 1;
else return;
e.preventDefault();
setActive(next);
refs.current[next].focus(); // move REAL focus to the new item
}
return (
<ul role="menu" onKeyDown={onKeyDown}>
{items.map((label, i) => (
<li key={label} role="menuitem"
tabIndex={i === active ? 0 : -1} // exactly one 0 -> single tab stop
ref={el => (refs.current[i] = el)}>
{label}
</li>
))}
</ul>
);
}