Accessibility · Semantics and the A11y Tree

Focus Order and the Accessibility Tree

How tabindex, DOM order, and the tree order diverge, and why the tab sequence and the reading order are computed from different structures.

Two orders exist on every page and they are not the same. The focus order is the sequence produced by pressing Tab: a traversal of focusable elements. The reading order is the sequence an assistive technology follows in browse mode: a traversal of accessibility objects. They are computed from different structures, they can be changed by different declarations, and a page can be internally consistent in each while being incoherent between them.

The divergence is not a bug. Focus order answers “what happens when the user presses Tab”, and its correct answer depends on interactive affordances, which are a property of controls. Reading order answers “what does the document say, in what sequence”, and its correct answer depends on semantics, which is a property of content. A layout can legitimately require a visual arrangement that does not match either, and the two orders can be repaired independently.

What makes an element focusable

An element participates in sequential focus navigation if it is focusable and not inert, and focusability comes from three sources: being natively interactive (a with href, button, input not of type hidden, select, textarea, summary inside details, iframe), having a tabindex value, or being an editing host (contenteditable).

tabindex has three regimes. A positive value puts the element in the tab order ahead of all elements with a value of zero or no value, sorted by the number, with ties broken by document order. A value of zero puts the element in the tab order at its document position, alongside the native controls. A value of negative one makes the element focusable by script and by click but removes it from the tab sequence.

The positive-value regime is the one that breaks pages. A single tabindex="1" moves its element to the front of the entire document’s sequence, ahead of every native control. Two elements with values 1 and 2 produce an order determined by those numbers and then a jump back to the natural sequence, so the tab position moves forward, then backward, then forward again. The values are document-global, not scoped to a container, so a component that uses tabindex="1" internally reorders the host page. The recommendation to use only 0 and -1 follows from this: the positive range has no compositional semantics.

Why the tab order is not the tree order

The accessibility tree follows the flattened tree: the DOM plus shadow roots plus slots, with aria-owns re-parenting. The tab order follows the flat tree too, but it is filtered by focusability, and the filter does not align with the tree’s parent-child structure in one important case: a focusable element inside a non-focusable container is a leaf in the tab order but an interior node in the tree.

Consider a div containing three links. The tree is a generic container with three link children. The tab sequence visits the three links in order. The container is not a stop. This is the normal case and the orders agree, because the tree traversal in browse mode also visits the links in that order.

Now consider CSS reordering. flex-direction: row-reverse or order: -1 on a flex item changes the visual order without changing the DOM order. The accessibility tree follows the DOM order, not the visual order, because the tree is built from the box tree’s structure but ordered by the document. The tab order also follows the DOM order. So both orders agree with each other and disagree with what a sighted keyboard user sees: Tab moves right to left while the eye moves left to right.

That is a real failure, and it is a failure of the visual order rather than of either computed order. It is also the reason the CSS specifications warn that order and grid placement affect only painting and not the logical order. Reordering that matters for meaning must be done in the markup.

aria-owns and the re-parented subtree

aria-owns is the one mechanism that makes the tree order differ from the tab order in a way that cannot be fixed by markup order. It re-parents objects in the accessibility tree, so a browse-mode traversal follows the new parent, while the tab sequence follows the DOM and continues to visit the owned elements at their original document positions.

The standard case is a combobox whose listbox lives at the end of the document, outside the combobox’s DOM subtree. aria-owns makes the listbox a child of the combobox in the tree, so the tree order is combobox then listbox. The tab order does not include the listbox’s options at all; they are managed with aria-activedescendant rather than real focus, so there is no conflict in that case. The conflict appears when the owned content contains focusable elements: the tree says they come immediately after the owner, the tab sequence says they come wherever they are in the document.

The resolution is that neither order is authoritative and both must be checked. A tree that is correct and a tab order that is correct can still produce a page where the two disagree, and the user experiences the disagreement as a jump.

// Focus order is a document-order traversal of focusable elements.
// The accessibility tree order can differ when aria-owns re-parents nodes.
const focusables = [...document.querySelectorAll('a[href], button, input, select, textarea, [tabindex]')]
  .filter(el => el.tabIndex >= 0 && !el.closest('[inert]'));
// focusables is in document order; tabindex > 0 would reorder it, and
// aria-owns has no effect on this list at all.

The tabindex=“0” on a container problem

Giving a container tabindex="0" is a common way to make a scrollable region reachable by keyboard. It also inserts a stop that announces the container’s role and name, which is usually the intent. The mechanism works because a focusable object with a generic role becomes a focusable generic object, and a name can then be attached. The exception is that a generic object cannot take a name, so the container is announced as an unnamed group or as nothing at all, depending on the engine.

The pattern that works is to give the container a role that supports naming: region with an aria-label, or group. Then the focus stop has something to say. The scrollable region is then reachable, named, and announced as a landmark or group. The rule that a generic element cannot be named, set out in Implicit Roles and How Role Conflicts Resolve, is what makes the difference between a working and a silent focus stop.

A second problem with container focus is that focus inside the container does not move the tree position. When focus lands on a scrollable div, the screen reader’s virtual cursor may stay where it was, because browse mode and focus mode are separate. Users of screen readers that follow focus get the container’s announcement; users of screen readers that stay in browse mode get nothing. The inconsistency is a property of the consumer, not of the page, which is why the pattern is tested with more than one screen reader.

Roving tabindex and the single-stop pattern

Composite widgets (toolbars, tab lists, grids, tree views) cannot put every child in the tab sequence. A toolbar with twenty buttons would cost twenty Tab presses to leave. The convention is that the composite is one tab stop, and arrow keys move focus among its children.

Roving tabindex implements this by giving exactly one child tabindex="0" and the rest tabindex="-1". The Tab key enters the widget at the current child, arrow keys move focus and transfer the zero to the newly focused child. The tree structure is unaffected: all children remain objects with their roles, and the roving attribute changes only the focus sequence. The related pattern, aria-activedescendant, keeps DOM focus on the container and reports the active child through an attribute, so the tab sequence has exactly one stop and the tree still contains every child.

The trade-off between the two is about where focus lives. Roving tabindex puts real focus on the child, so the browser’s focus ring, :focus-visible, and scroll-into-view all work without intervention, and assistive technologies that track the focused element see the child directly. aria-activedescendant keeps focus on the container, so the container must be focusable, must carry the active child’s identity, and must manage the announcement itself; in exchange, the container can handle key events centrally and the focus ring can be drawn on the active child by CSS. The tree is identical either way. Only the focus sequence differs.

Order is a property of the whole page

The final constraint is that the two orders are global. A component cannot define its own tab order in isolation, because the tab sequence is computed over the document, and a positive tabindex anywhere in a component changes the sequence everywhere. Shadow DOM does not change this: focus navigation crosses shadow boundaries in the flattened tree, so a component’s internal tabindex values are visible to the outer sequence.

The practical consequence is that the only compositional tabindex values are 0 and -1, and that any other value is a page-level decision. The tree order is more forgiving: aria-owns is scoped to the owner and its subtree, so a component can re-parent its own content without affecting the rest of the page. The mechanisms that change tree structure without changing the DOM, and the event pipeline that keeps both orders synchronised after a change, are treated in Accessibility Tree Invalidation and Event Coalescing.

References

  1. HTML Standard — Sequential focus navigation — WHATWG
  2. CSS Flexible Box Layout Module Level 1 — Ordering — W3C
  3. CSS Grid Layout Module Level 1 — Order — W3C
  4. WAI-ARIA Authoring Practices Guide — W3C