Arrow keys inside a widget: roving tabindex or aria-activedescendant
How a tab list, toolbar, listbox or grid takes one Tab stop and moves with the arrow keys, by a roving tabindex or by aria-activedescendant, and when each one fits.
In short: A composite widget takes one Tab stop, and the arrow keys move inside it. Either move real focus with a roving tabindex, tabindex="0" on the active item and "-1" on the rest, or keep focus on the container or input and point aria-activedescendant at the active item. An editable combobox needs the second, because focus has to stay in its input.
01
When you need this
A recreated note-taking app has a formatting toolbar with four buttons (Undo, Redo, Insert link, Insert image) and a Tags field that suggests tags as someone types: "des" brings up Design, Design review and Desk research. A keyboard test comes back with two notes:
- Tab reaches Undo, then nothing: the other three tools take no focus, and the arrow keys do nothing.
- In Tags, the Down Arrow moves focus into the list. The next letter typed goes nowhere.
Both are widgets with several parts inside one Tab stop. The question is how the arrow keys should move inside them, and which of the two techniques each one needs.
02
One Tab stop, arrow keys inside
The ARIA Authoring Practices Guide (APG) gives the convention all platforms share: "the tab and shift + tab keys move focus from one UI component to another while other keys, primarily the arrow keys, move focus inside of components that include multiple focusable elements." WAI-ARIA calls such a component a composite widget, and controlling focus inside it "managing focus". The APG says the tab sequence "should include only one focusable element of a composite UI component."
Where focus lands when Tab enters the widget depends on its type (APG, Keyboard Navigation Inside Components):
- The element that had focus last time, or the first one: grid and treegrid.
- The selected element, or the first one: radio group (the checked button), tabs, listbox and tree.
- The first element: menubar and toolbar. The toolbar pattern allows the control that last had focus instead.
Inside, each APG pattern names its keys. The authors are "free to choose which keys move focus inside of a composite, but they are strongly advised to use the same key bindings as similar components in common GUI operating systems":
| Pattern | Arrow keys | Home and End |
|---|---|---|
| Tabs, horizontal | Left and Right, going round from last to first; Up and Down left to scroll the page | Optional: first and last tab |
| Toolbar, horizontal | Left and Right; going round at the ends is optional | Optional: first and last control |
| Radio group | All four move focus and check the button, going round | Not in the pattern |
| Listbox | Up and Down | Optional, and strongly recommended for more than five options |
| Tree view | Up and Down; Right opens a node or goes into it; Left closes it or goes to its parent | First and last node |
| Grid | All four, one cell at a time | Home and End: the ends of the row; Control + Home and Control + End: the first and last cell |
Listboxes and trees also recommend type-ahead: a typed letter moves to the next item starting with it. The guides that show each pattern with a live demo are tabs that only work with a mouse, toolbars of clickable icons a keyboard can't reach, star ratings that keyboards and screen readers skip (a radio group), tree views keyboards can't open and screen readers flatten, editable data grids a keyboard can't move through or edit and seat maps keyboards can't use and screen readers can't read (both grids).
03
Roving tabindex: moving real focus
With a roving tabindex, the item that is to be in the tab sequence has tabindex="0" and every other item in the widget has tabindex="-1", which takes it out of the sequence but leaves it focusable from script. The APG's steps:
- When the widget is created, set
tabindex="0"on the item to start with andtabindex="-1"on the rest. - On an arrow key, set
tabindex="-1"on the item that has0, andtabindex="0"on the item focus is moving to. - Call
focus()on that item.
The toolbar from the scene, done this way:
<div role="toolbar" aria-label="Formatting">
<button type="button" tabindex="0">Undo</button>
<button type="button" tabindex="-1">Redo</button>
<button type="button" tabindex="-1">Insert link</button>
<button type="button" tabindex="-1">Insert image</button>
</div>const toolbar = document.querySelector('[role="toolbar"]');
const tools = [...toolbar.querySelectorAll('button')];
// Give the one Tab stop to this tool.
function setStop(tool) {
for (const t of tools) t.tabIndex = t === tool ? 0 : -1;
}
toolbar.addEventListener('keydown', (event) => {
const i = tools.indexOf(document.activeElement);
const to = { ArrowRight: i + 1, ArrowLeft: i - 1, Home: 0, End: tools.length - 1 }[event.key];
if (to === undefined) return;
event.preventDefault();
const tool = tools[(to + tools.length) % tools.length];
setStop(tool);
tool.focus();
});
// A click moves the Tab stop too, in browsers that focus a clicked button.
toolbar.addEventListener('focusin', (event) => {
if (tools.includes(event.target)) setStop(event.target);
});Because focus really moves, the browser scrolls the item into view, draws its own focus ring on it, and :focus-visible styles it; the APG names the scrolling as one benefit of this technique. Each item keeps the keys it has natively, so Enter and Space still press a button. The APG adds that a click or a tap should set the tabindex just as a key does, or Shift + Tab back into the widget lands somewhere the person didn't leave.
04
aria-activedescendant: focus stays, the active item moves
With aria-activedescendant, only the container (or the input that controls it) is in the tab sequence, and DOM focus stays there. Its value is the id of the active item. WAI-ARIA 1.2 §4.3 says that the browser "keeps the DOM focus on the container element or on an input element that controls the container element", but "communicates desktop focus events and states to the assistive technology as if the element referenced by aria-activedescendant has focus." Key events still go to the element with DOM focus, so the script handles them there.
Three things become the author's job:
- The highlight. "The author, not the user agent, is responsible for styling the currently active descendant to show it has keyboard focus. The author cannot use :focus to style the currently active descendant since the actual focus is on the container" (WAI-ARIA 1.2 §4.3.1).
- Scrolling. The APG says to scroll the active item into view as it changes; WAI-ARIA says authors SHOULD keep it "visible and in view".
- Where the item lives. WAI-ARIA 1.2 requires the referenced item to be owned: a DOM descendant of the focused element, or named in its
aria-owns. The one exception: when the focused element is a combobox, textbox or searchbox witharia-controlspointing at the popup, the item may be in that popup.
The property is used on application, combobox, composite, group and textbox, and roles that inherit from those, such as listbox, grid, tree, tablist and toolbar. The Tags field from the scene, done this way:
<label for="tags">Tags</label>
<input id="tags" type="text" role="combobox"
aria-autocomplete="list" aria-expanded="true"
aria-controls="tag-list" aria-activedescendant="tag-1">
<ul id="tag-list" role="listbox" aria-label="Suggested tags">
<li id="tag-0" role="option" aria-selected="false">Design</li>
<li id="tag-1" role="option" aria-selected="true">Design review</li>
<li id="tag-2" role="option" aria-selected="false">Desk research</li>
</ul>const input = document.getElementById('tags');
const options = [...document.querySelectorAll('#tag-list [role="option"]')];
function move(index) {
options.forEach((option, i) => option.setAttribute('aria-selected', String(i === index)));
input.setAttribute('aria-activedescendant', options[index].id);
options[index].scrollIntoView({ block: 'nearest' });
}/* Focus stays in the input, so the highlight comes from the state, never from :focus. */
#tag-list [aria-selected="true"] {
outline: 2px solid currentColor;
}In the APG's listbox popup, selection follows focus, so the active option is also the selected one. The combobox guide has the full script: opening, closing, Enter, Escape and the count of suggestions.
05
Which one to use
The APG describes both for managing focus inside a composite widget. The choice follows from where focus has to be:
| The widget | Use | Why |
|---|---|---|
| An editable combobox: a field with suggestions | aria-activedescendant |
People keep typing while the list is open, so DOM focus stays in the input. The APG combobox pattern: "DOM Focus is maintained on the combobox" |
| A select-only combobox: a custom dropdown | aria-activedescendant, as the APG combobox pattern describes for its listbox popup |
The trigger keeps focus while the arrow keys move through the list, as in custom dropdowns that keyboards and screen readers can't use |
| Tabs, toolbar, radio group, tree, grid | Either; the guides here use a roving tabindex | Real focus brings the browser's own focus ring and scrolling, and keeps each item's native keys |
Some cases decide it for you:
- Items that are controls with keys of their own, such as a button in each cell of a seat map: with
aria-activedescendant, key events go to the container (WAI-ARIA 1.2 §4.3), so a native button inside never gets its own Enter and Space. A roving tabindex lets each one keep them. - A long list built on demand: an item that isn't in the DOM can't take focus or be referenced, so render it before moving to it, whichever technique you use.
06
What screen readers say, and focus that follows the page
With a roving tabindex, focus really moves, and the screen reader reads the newly focused item from the tree: its name, role and states, worded in its own way. With aria-activedescendant, WAI-ARIA 1.2 §4.3.2 has the browser "fire an accessibility API desktop focus event on the new element referenced by aria-activedescendant", and the APG says assistive technologies "will receive focus change events equivalent to those received when DOM focus actually moves." On paper, the screen reader is told the same thing either way.
Support is where they can differ:
- EaseWeb's own results: as of 2 November 2026, screen reader support has no result yet for
aria-activedescendantor for a roving tabindex. Its test of arrow keys between the buttons of a grid is listed with no result yet (grid with a button in each cell). - A published report, not an EaseWeb test: WebKit bug 264891, filed in November 2023, reported that VoiceOver with Safari didn't announce the option an editable combobox's
aria-activedescendantpointed to, on the APG's own examples, while the reporter said NVDA and JAWS did. WebKit closed it in April 2024 as a duplicate of bug 272565, whose fix was committed that month, noting that checking it needed a newer VoiceOver. - Reading mode: screen readers use the arrow keys themselves while reading. Whether they hand them to the widget depends on the role and the screen reader; the keys that switch modes are on screen reader shortcuts.
Whichever technique you use, focus has to follow what the page shows:
- The highlight and the active item are always the same item. "The visual focus indicator must always be visible", and "the selected state must be visually distinct from the focus indicator" (APG). With
aria-activedescendant, the browser's ring is on the input, so the highlight you draw is the only sign of where the person is. - Focus is never lost. If the focused item is removed, move focus to a logical element (WAI-ARIA 1.2 §4.3.1); a widget that loses it sends focus to the body.
- Coming back finds the same place. WAI-ARIA recommends that a container refocused later has the same active item as when it was left, except where the content has changed or, as in a menubar, people expect the first item.
- Clicks and keys agree. A click sets the
tabindexoraria-activedescendantas a key would.
07
Worked example
The two notes from the scene, fixed and tested:
- The toolbar had
tabindex="-1"on every tool but Undo, copied from the toolbar pattern without its script, so the other three were out of reach. Adding the keydown and focusin handlers above made it a roving tabindex. - The Tags field moved DOM focus into the list on the Down Arrow, so the next letter went nowhere. The fix kept focus in the input, gave the list the combobox roles, and moved
aria-activedescendantinstead.
The keyboard test, run again:
| Check | Result |
|---|---|
| Tab from the page above | Lands on Undo; one more Tab goes on to Tags |
| Right Arrow three times, then once more | Redo, Insert link, Insert image, then round to Undo |
| Home and End in the toolbar | Undo and Insert image |
| Click Insert link, Tab away, then Shift + Tab back | Back on Insert link, in a browser that focuses a clicked button |
| Type "des" in Tags, then Down Arrow | Focus stays in the field; Design is highlighted and aria-activedescendant is tag-0 |
| Type "k" with the list open | The letter goes into the field and the list narrows to Desk research |
What this table doesn't show is what each screen reader says. That needs a test in each combination the site supports, recorded with versions and the date.
08
Common mistakes
tabindex="0"on every item. Tab stops on each one, and the arrow keys a grid or tree promises still do nothing. See editable data grids a keyboard can't move through or edit.- The
tabindex="-1"markup without the script. Every item but one is out of reach, which is worse than leaving them all in the tab sequence. See toolbars of clickable icons a keyboard can't reach. - Moving DOM focus into a combobox's list. The next letter typed has nowhere to go. Keep focus in the input and move
aria-activedescendant. See autocomplete fields with silent, mouse-only suggestions. - A highlight drawn from a class, or from
:focus, witharia-activedescendant.:focusnever reaches the item, and a class can show one item while the attribute names another. Draw it fromaria-selectedor the attribute itself. See custom dropdowns that keyboards and screen readers can't use. aria-activedescendantpointing at an item that isn't owned, or isn't in the page. WAI-ARIA requires an owned element; if the id names nothing in the document, the browser fires the focus event on the container instead (§4.3.2).- Roles without keys.
role="tree"androle="treeitem"on clickable rows say "tree" and still answer no arrow key. See tree views keyboards can't open and screen readers flatten. - Selection following focus when each change is slow. The APG warns that when showing a tab's panel needs a request or a reload, each arrow press makes the person wait; show it on Enter or Space instead. See tabs that only work with a mouse.
- A click that doesn't move the Tab stop. Shift + Tab back into the widget lands on an item the person never chose. See star ratings that keyboards and screen readers skip.
09
What the standard says
- WCAG 2.2, required:
- 2.1.1 Keyboard (Level A): "All functionality of the content is operable through a keyboard interface without requiring specific timings for individual keystrokes", except where the function depends on the path of the user's movement.
- 2.4.3 Focus Order (Level A): "If a web page can be navigated sequentially and the navigation sequences affect meaning or operation, focusable components receive focus in an order that preserves meaning and operability."
- 2.4.7 Focus Visible (Level AA): "Any keyboard operable user interface has a mode of operation where the keyboard focus indicator is visible."
- None of them asks for one Tab stop or for the arrow keys. A toolbar that leaves every button in the tab sequence passes 2.1.1, as the toolbars guide shows; the single Tab stop is the APG's convention. EN 301 549 V3.2.1 has the same three as 9.2.1.1, 9.2.4.3 and 9.2.4.7, JIS X 8341-3:2016 has them as 2.1.1, 2.4.3 and 2.4.7, and Section 508 (E205.4) requires WCAG 2.0 Level A and AA, which includes them.
- WAI-ARIA 1.2 (W3C Recommendation, 6 June 2023), required of content that uses ARIA:
- §4.3.1: "Authors MUST manage focus on the following container roles: grid, listbox, menu, menubar, radiogroup, tree, treegrid, tablist."
aria-activedescendant: "Authors MUST ensure" the referenced element is owned by the focused element, or, for a combobox, textbox or searchbox witharia-controls, by the element it controls. Authors "SHOULD also ensure that the currently active descendant is visible and in view (or scrolls into view) when focused."
- The APG, advice: the keys, where focus lands, and the two techniques are recommended practice, not requirements.
10
Limits
This page covers how focus moves inside a widget. It doesn't settle:
- What each screen reader says with each technique. That is tested combination by combination, and the results age; see screen reader support.
- Controls a keyboard can't reach at all, such as a
divwith only a click handler: see div buttons that can't be reached with Tab. - Roles, names and states: see what a screen reader gets from your HTML and ARIA.
- Shortcuts that work across the page, which the APG covers separately: see single-key shortcuts that can't be turned off.
- Touch screens. People using a screen reader on a phone move by swiping, not with arrow keys, so test the widget there as well.
Whether a widget's keys suit its users, and whether a team's own component library gets them right, is for the tester to check on the real page.
11
Terms
- composite widget
- A widget that holds other focusable parts and takes one Tab stop, such as a tab list, toolbar, listbox, radio group, grid or tree.
- tab sequence
- The path focus follows when Tab and Shift + Tab are pressed.
- roving tabindex
- Moving real focus inside a widget: the active item has tabindex="0", every other item tabindex="-1", and the arrow keys move both the 0 and focus().
- aria-activedescendant
- An ARIA property on the focused container or input whose value is the id of the active item, which assistive technology then treats as focused.
- DOM focus
- Focus as the browser holds it, on the element document.activeElement returns, where key events go.
- selection follows focus
- Moving focus to an item also selects it, as tabs that show each panel as the arrow keys reach its tab.
12
Sources and guides
- Advice ARIA Authoring Practices Guide: Developing a Keyboard Interface: Fundamental Keyboard Navigation Conventions (opens in a new tab)
- Advice ARIA Authoring Practices Guide: Developing a Keyboard Interface: Keyboard Navigation Inside Components (opens in a new tab)
- Advice ARIA Authoring Practices Guide: Managing Focus Within Components Using a Roving tabindex (opens in a new tab)
- Advice ARIA Authoring Practices Guide: Managing Focus in Composites Using aria-activedescendant (opens in a new tab)
- Advice ARIA Authoring Practices Guide: Combobox Pattern: Keyboard Interaction (opens in a new tab)
- Required WAI-ARIA 1.2: 4.3 Managing Focus and Supporting Keyboard Navigation (opens in a new tab)
- Required WAI-ARIA 1.2: 6.7 aria-activedescendant (opens in a new tab)
- Required WCAG 2.2: 2.1.1 Keyboard (opens in a new tab)
- Required WCAG 2.2: 2.4.3 Focus Order (opens in a new tab)
- Required WCAG 2.2: 2.4.7 Focus Visible (opens in a new tab)
Guides that show it
- Tabs that only work with a mouse Product tabs are list items with click handlers, so they take no keyboard focus and say nothing about which panel is shown.
- Toolbars of clickable icons a keyboard can't reach A photo editor's tools are icons in spans with click handlers, so Tab passes the whole toolbar and nothing tells a screen reader the tools are there.
- Tree views keyboards can't open and screen readers flatten A sidebar's sections are clickable divs, or tree roles without their keys and levels, so keyboards can't open them and a screen reader hears one flat list.
- Star ratings that keyboards and screen readers skip A review form's five stars are clickable spans or radio buttons hidden with display: none, so Tab never reaches them and a screen reader hears no rating.
- Editable data grids a keyboard can't move through or edit The cells of a table of values turn into fields only on a click, so the keyboard can't reach a cell and the field that opens has no name.
- Autocomplete fields with silent, mouse-only suggestions A type-ahead field's suggestions are a plain list with click handlers or a CSS highlight, so a keyboard can't pick one and screen readers never hear them.
- Custom dropdowns that keyboards and screen readers can't use A dropdown rebuilt from divs takes only clicks and has a label nothing points to, so keyboards can't choose and screen readers hear no label or options.
- Seat maps keyboards can't use and screen readers can't read Each seat is an empty div that takes only a click, or a button with no name, and free, taken and chosen seats differ only in colour.
This page explains a practice, with recreated examples. It is not a screen reader test result, and it is not a finding about a named client.