Accessibility · Semantics and the A11y Tree

Container Roles and Nested Structure

How group, region, and composite containers nest, what nesting does to landmark and heading navigation, and when a wrapper should carry a role at all.

A container role does nothing on its own. group, region, list, tablist, grid, and the landmark roles exist to give their contents a boundary that a consumer can address: a thing to name, to enter and leave, to count the children of, or to jump to. The design question is always the same: should this wrapper be a boundary at all, and the answer is determined by what the consumer can do with a boundary, not by the visual grouping the author had in mind.

A page with no container roles still has a tree: a flat sequence of headings, paragraphs, links, and controls under a root. A screen reader can navigate it by heading, by link, by control, and by text. It cannot navigate by region, because there are none, and it cannot report “12 items” for a list that has no list role. Adding a boundary buys one navigation axis and costs one level of indirection in every traversal that crosses it.

Landmarks are a fixed vocabulary

There are eight landmark roles: banner, main, complementary, contentinfo, navigation, region, search, and form. Each corresponds to a native element or attribute: header and footer at the top level, main, aside, nav, a named section, search, and a named form. The native mapping is preferred because it is exposed without a role attribute and because the elements carry other semantics: a nav is navigation whether or not it is named.

The constraint that makes landmarks useful is that they are page-level. A screen reader’s landmark list is a list of all landmarks in the document, and it is expected to be short enough to scan. A page with three landmarks has a useful table of contents. A page with thirty has a list as long as the document and no better than scrolling.

Nesting is where the vocabulary runs out. A nav inside a nav is legal and both are exposed; a consumer that lists navigation landmarks shows two entries and the user must infer which is the sub-navigation. region is the general-purpose landmark and the one most often overused, because it is the only landmark role that can be applied to any element. Every section with an accessible name becomes a region, so a design system that names every section produces a landmark per section.

The containment rules are not enforced. main should not be inside banner, and a banner inside main is not a banner at all, because the role is defined only for the body-level header, and HTML-AAM maps a nested header to generic. An explicit role="banner" on a nested element is honoured by some engines and ignored by others, because the mapping for the element and the mapping for the role disagree. The disagreement is the general problem of container roles: the role is a claim about position, and position is not something the role can verify.

group is the null container

group is the container role with no navigation semantics. It does not appear in landmark lists, it has no keyboard behaviour, and a consumer announces it only as a boundary if it has a name. It exists so that a set of related controls can be named as a set: a fieldset without a legend, a toolbar-like cluster, a set of radio buttons that share a label.

The useful property of group is that it is cheap. An unnamed group is exposed but produces no announcement, so it can be used to attach a name to a subtree without adding a navigation stop. A named group is announced on entry and on exit, which is exactly the cost of the name. A group that contains a single control adds an announcement that carries no information; a group that contains fifteen controls and a shared label carries a great deal.

group inside group nests without limit and without repair, because the role has no required children and no required parent. That makes it the safe choice for structural wrappers that need a name and nothing else, and the wrong choice for anything that a consumer should be able to navigate to directly.

Composite containers carry state

tablist, listbox, grid, tree, menu, radiogroup, and toolbar are containers with behaviour. Their children are focusable in a roving or activedescendant model, and the container is responsible for the keyboard interaction that moves among them. The container’s role is what tells the consumer that arrow keys apply.

The structural consequence is that these containers cannot be nested arbitrarily. A tablist inside a tablist is not meaningful: the inner tabs would be owned by the inner tablist and the outer one would have no tabs, violating its required-children constraint. A grid inside a grid is legal (nested grids are used for split layouts), but the arrow-key model becomes ambiguous, because the outer grid’s navigation and the inner grid’s navigation both claim the same keys. Consumers resolve this by entering the inner grid on Right and leaving it on Left, which is a convention rather than a specification.

A group between a composite container and its items is the case that breaks the grammar. A tablist containing a group containing tab elements: the group is not ignored because it has a role, so the tabs’ tree parent is the group, and the tabs violate their required-parent constraint. The engine either exposes the tabs as orphans or synthesises a tablist. The fix is to remove the group, or to move the label onto the tablist itself, since a tablist can be named. The general rule, set out in Required Owned Elements and Structural Constraints, is that the container’s own name is the correct place for the label, not an intermediate wrapper.

<div role="tablist" aria-label="Settings sections">
  <!-- correct: tabs are direct children, tablist carries the name -->
  <button role="tab" aria-selected="true" aria-controls="panel-a">General</button>
  <button role="tab" aria-selected="false" aria-controls="panel-b">Privacy</button>
</div>

<div role="group" aria-label="Settings sections">
  <div role="tablist">
    <!-- wrong: the group is now the tabs' parent, not the tablist -->
    <button role="tab" aria-selected="true">General</button>
  </div>
</div>

What nesting does to heading navigation

Headings are not containers, but they are the other navigation axis, and the two interact. A screen reader’s heading list is flat: every heading at every level, in document order. The level is exposed as a number and the consumer may filter by it. Nesting a section inside a section does not create a hierarchy in the heading list, because the list is derived from the heading objects, not from the containers around them.

The consequence is that container roles and headings duplicate each other. A page can express its structure with landmarks, with headings, or with both, and the two axes are navigated separately. A region named by its heading (the section element’s implicit naming from its h2) appears in both lists, so the user can reach the same place two ways. That redundancy is useful when the landmark list is short and harmful when it is long.

The interaction with aria-labelledby is the mechanism: a section whose aria-labelledby points at its own h2 takes the heading’s text as its name, and the name appears in the landmark list. Without the reference, the section is unnamed and therefore generic, and the heading is the only way in. The name computation that produces this is the same cascade described in The Order of Sources in Accessible Name Computation; the container case is just the one where the content-derived name is the usual choice.

Counting children

A list announces its item count; a listbox announces the number of options; a grid announces rows and columns. The counts are computed from the tree, which means they follow the tree’s structure rather than the markup’s. A list whose items are wrapped in divs counts the items correctly if the wrappers are ignored; if the wrappers carry role="group", the list’s children are groups, and the count is the number of groups.

The count is also affected by aria-owns and by hidden children. A listbox with twenty options of which five are aria-hidden reports fifteen. A listbox that owns options from elsewhere in the document reports them as its own. Neither behaviour is wrong, and both surprise authors who count elements in the DOM.

The final structural question is whether the count is worth a container at all. A set of three links in a paragraph does not need to be a list; the count adds an announcement and no navigation. A set of thirty results does, because the count tells the user how far the traversal will go. The threshold is not a number but a consequence: a container is worth its cost when a consumer can act on the boundary by jumping to it, counting it, or traversing it with a dedicated key, and not otherwise.

References

  1. WAI-ARIA 1.2 — Landmark Roles — W3C
  2. WAI-ARIA Authoring Practices Guide — Landmark Regions — W3C
  3. HTML Accessibility API Mappings 1.2 — W3C
  4. Core Accessibility API Mappings 1.2 — W3C