Skip to content
easeweb

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:

  1. When the widget is created, set tabindex="0" on the item to start with and tabindex="-1" on the rest.
  2. On an arrow key, set tabindex="-1" on the item that has 0, and tabindex="0" on the item focus is moving to.
  3. 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 with aria-controls pointing 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-activedescendant or 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-activedescendant pointed 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 tabindex or aria-activedescendant as a key would.

07

Worked example

The two notes from the scene, fixed and tested:

  1. 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.
  2. 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-activedescendant instead.

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

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 with aria-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:

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

Guides that show it

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.