Custom widget keyboard contracts
If you ship a menu, a tab list, a combobox, or a tree, users expect specific keys to do specific things.
You can't build a custom dropdown the way you build a <button> — by adding role="menu" and calling it done. ARIA roles tell screen readers what something is. They don't tell the keyboard what to do. If you say a thing is a menu, users expect it to behave like a menu — and "behave like a menu" is a specific, written-down keyboard contract.
This lesson is a quick tour of the most common contracts from the ARIA Authoring Practices Guide (APG), the standard reference for how custom widgets should respond to keys. We won't memorize every key. The goal is to recognize the shape of these contracts so you know where to look when you build one.
The roving-tabindex principle
Most of these widgets share a single trick. Inside a menu, a tab list, a listbox, or a tree, only one item is in the tab order at a time. The user tabs to the widget, then uses arrow keys inside it.
This is called roving tabindex: every item has tabindex="-1" except the currently active one, which has tabindex="0". When the user presses an arrow key, you move the 0 to the next item.
function focusItem(items: HTMLElement[], next: number) {
items.forEach((el, i) => {
el.setAttribute("tabindex", i === next ? "0" : "-1");
});
items[next].focus();
}The alternative — having every item in the tab order — forces the user to tab through every menu option just to leave the menu. That's bad for menus of 10 things, terrible for menus of 50.
When you see a widget with arrow-key navigation in the spec, it's almost always roving tabindex. Build the helper once and reuse it across components.
A menu button opens a menu of actions (think: an actions menu in a row of a table). The keyboard contract:
- Enter or Space on the trigger button opens the menu.
- Down opens the menu and focuses the first item.
- Up opens the menu and focuses the last item.
- Inside the menu, Down/Up move between items, wrapping at the ends.
- Home jumps to the first item, End to the last.
- A printable character jumps to the next item starting with that letter (typeahead).
- Enter activates the focused item and closes the menu.
- Esc closes the menu and returns focus to the trigger.
- Tab closes the menu and moves focus past the trigger normally.
The "Tab closes the menu" behavior is the tell that this isn't a focus trap — a menu isn't modal. Tab is a way out.
Tabs
Tabs (a list of buttons that switch which panel is visible) follow a simpler contract:
- Tab moves focus to the active tab (only one tab is in the tab order — roving again).
- Right/Left move between tabs (or Down/Up for vertical tabs).
- Home to the first tab, End to the last.
- Tab again moves into the panel content.
A real choice: do tabs activate on focus (automatic) or only on Enter/Space (manual)? Automatic feels faster but loads content for every tab the user passes through. Manual is gentler and avoids unnecessary work. APG says either is acceptable; pick the one that matches your panel cost.
Combobox and listbox
A listbox is a list where the user can pick one (or more) options — think a custom version of <select multiple>. A combobox is a text input paired with a popup that suggests options (autocomplete, search-as-you-type).
Listbox keys:
- Down/Up move between options.
- Home/End jump to first/last.
- Space selects the focused option (in single-select, often the same as Enter).
- For multi-select: Shift+Click or Shift+Down extends selection.
Combobox keys (combined with the input):
- Down opens the popup and focuses the first option.
- The user can keep typing — the input remains the active element; arrow keys navigate the popup visually, with
aria-activedescendanttelling assistive tech which option is current. - Enter picks the highlighted option and puts its value in the input.
- Esc closes the popup without changing the input.
Comboboxes are notoriously tricky — at least four ARIA-supported variants exist (autocomplete-list, autocomplete-inline, autocomplete-both, no-autocomplete). Look at the APG combobox patterns before building one from scratch.
Don't invent your own combobox keyboard model. Match the APG patterns. Users have built up muscle memory from native <select> and dozens of other comboboxes — non-standard keys feel broken even when they work.
Tree view
A tree (a file browser, a category hierarchy) extends the list contract with expand/collapse:
- Down/Up move between visible nodes.
- Right on a collapsed node expands it. On an expanded node, moves to the first child.
- Left on an expanded node collapses it. On a collapsed (or leaf) node, moves to the parent.
- Home to the first node, End to the last visible node.
- A typed letter jumps to the next visible node starting with it.
The expand/collapse-vs-navigate dance is what makes trees a tree and not a list. If you skip it, your "tree" is really a flat list — that may be fine, but don't use role="tree" for it.
Try it yourself
aria-activedescendant in a combobox instead of moving real focus to each option?