Accessibility · Semantics and the A11y Tree

Hidden Subtrees and Presentational Roles

The difference between aria-hidden, display none, visibility hidden, and role presentation, and which of them removes a node from the tab order.

Four mechanisms remove something from the accessibility tree, and they are not interchangeable. Each one removes a different amount, at a different stage, and with different consequences for focus, for naming, and for the layout that sighted users see. Confusing them is the most common source of elements that are invisible to a screen reader but still reachable by keyboard, or visible but silent.

The four are: layout removal (display: none), paint removal (visibility: hidden), tree removal (aria-hidden="true"), and role removal (role="presentation" or role="none"). A fifth mechanism, the hidden attribute, is not a separate one; it applies display: none through the user-agent stylesheet and is therefore in the first category.

Layout removal and its side effects

display: none generates no box, so the element and its whole subtree have no geometry. An object with no geometry cannot be hit-tested, cannot be scrolled into view, and cannot be given a focus ring, so browsers exclude it from the accessibility tree entirely. The exclusion is total: no role, no name, no children. The subtree is not exposed in any form.

Because the element is not rendered, it is also not focusable. An element with tabindex="0" inside a display: none subtree is removed from the tab order, and a script that calls focus() on it does nothing. This coupling between rendering and focusability is what makes display: none the right tool for hiding a dialog that is closed: both the tree and the tab order agree that it is gone.

visibility: hidden also removes the object, but not the geometry. The element occupies its box in layout and contributes to the size of its parent, while generating no paint. Its accessibility object is excluded, and the element is not focusable. The difference from display: none is therefore purely spatial: a hidden-but-laid-out element leaves a gap, which is sometimes the intended effect and sometimes a layout bug that reveals itself only after the content is toggled.

Neither property respects a child’s attempt to opt back in, with one exception worth naming: a descendant of a visibility: hidden element can set visibility: visible and become both visible and exposed, because visibility is inherited but overridable. A descendant of a display: none element cannot, because there is no box tree in which to generate a box. That asymmetry is a genuine design difference, not an oversight.

aria-hidden without layout consequences

aria-hidden="true" removes the object and its subtree from the accessibility tree and changes nothing else. The element keeps its box, keeps its paint, and, critically, keeps its focusability. A focusable element inside an aria-hidden subtree remains in the tab order, and when focus lands on it, the platform’s focus tracking has to reconcile a focused object with no accessible representation.

Browsers resolve that reconciliation differently, and none of the resolutions is clean. Chromium exposes the focused element despite the ancestor’s aria-hidden, because hiding the focus target would make the focus event incoherent; the result is that the element is announced once focus reaches it and invisible to browse-mode navigation. Firefox has behaved similarly. The consequence for authors is that aria-hidden="true" on a container holding a focusable element produces an object that is silent until it is focused, then loud. The fix is to combine it with tabindex="-1" on the focusable descendants, or to use inert where it is supported, which removes both the tree and the focusability.

aria-hidden="false" does nothing. It does not re-expose a subtree hidden by an ancestor, because the hiding is inherited downward and the attribute is not a per-node override. Authors write it expecting a re-enable and get silence.

<div aria-hidden="true">
  <button>Close</button>          <!-- still tabbable, announced on focus -->
</div>

<div inert>
  <button>Close</button>          <!-- not tabbable, not in the tree -->
</div>

The inert attribute is the modern form of the first pattern done correctly: it removes the subtree from the tree, from the tab order, from hit-testing, and from find-in-page, in one declaration. Its support is now broad but not universal, which is why the older pattern persists.

A frequent use of aria-hidden is to suppress decorative glyphs: an icon font character, an emoji used as ornament, a duplicated visual label. Marking the glyph’s element hidden keeps it out of the computed name of its parent button. The alternative, role="presentation", does not remove the text from the name, because presentational roles drop semantics and keep content. An icon glyph that must not contribute to a name needs aria-hidden, not role="none".

Presentation and the refusal to comply

role="presentation" and role="none" request that an element’s own semantics be dropped. They do not request that the element be removed. The element’s children are re-parented into its parent, and its text contributes to whatever encloses it. A table with role="presentation" loses its table, row, and cell roles, and its text flows into the surrounding content; this was the standard technique for layout tables before CSS grid made them unnecessary.

The request is conditional, and the conditions are the interesting part. A presentational role is ignored, and the element’s implicit role restored, if any of the following holds:

  • The element is focusable, whether through tabindex, through being natively focusable, or through being the target of a script’s focus call.
  • The element has any global ARIA attribute, including aria-label, aria-labelledby, aria-describedby, aria-hidden, or a live region attribute.
  • The element has a state or property that requires a role.

The reasoning is that these conditions all imply the element is a thing in its own right. A focusable object with no role is a stop in the tab order that announces nothing. An object carrying a name is an object that something can refer to. Dropping the role in either case would produce a worse tree than keeping it.

The conditions are evaluated when the tree is built and re-evaluated when they change, by the same invalidation pass that recomputes any dirty object, described in Accessibility Tree Invalidation and Event Coalescing. Adding tabindex="0" to a presentational element later restores its implicit role and re-inserts it as a real object, which means the tree structure changes shape at that moment: children that had been re-parented move back under it. Scripts that toggle focusability on such elements are therefore also toggling tree structure, and a cached node reference held by an assistive technology can become stale.

display: contents, the accidental removal

display: contents removes the element’s own box but keeps its children’s boxes. The element therefore has no geometry, and by the rule that governs display: none, it has no object. Its children are exposed, re-parented into its parent.

The difference from role="presentation" is that the removal here is a layout consequence, not a semantic request, and it is unconditional. An element that combines display: contents with role="group" and aria-label loses all three: no object, no group, no name. The children keep their semantics, so the tree looks as if the wrapper never existed.

This is the mechanism behind the common failure where a list wrapper styled with display: contents to fix a flex layout silently drops the list semantics. The wrapper’s role is gone, the items are re-parented, and a screen reader reports the items as loose content rather than as a list. The behaviour is specified, an element with no box has no object, and it has been implemented consistently for long enough that treating display: contents as semantically transparent is correct.

The remedy is not to add role="list" back onto the wrapper, which would still have no box, but to move the semantics to an element that does have one, or to use a layout technique that keeps the wrapper’s box. Which of the four hiding mechanisms a given pattern should use follows from the table below.

MechanismRemoves objectRemoves descendantsRemoves focusabilityKeeps layout
display: noneyesyesyesno
visibility: hiddenyesyesyesyes (space reserved)
aria-hidden="true"yesyesnoyes
inertyesyesyesyes
role="presentation"nono (re-parented)noyes
display: contentsyesno (re-parented)nono box for the element

The row that surprises authors most is the third. Everything else follows from the rule that the tree is computed from boxes plus semantics, and that only one of the two inputs can be overridden by an attribute. The focus consequence of aria-hidden is covered from the other direction in Focus Order and the Accessibility Tree, where the ordering model that the tab sequence and the tree sequence share is set out.

References

  1. WAI-ARIA 1.2 — Presentational Roles Conflict Resolution — W3C
  2. CSS Display Module Level 3 — display: contents — W3C
  3. HTML Standard — The hidden attribute — WHATWG
  4. Core Accessibility API Mappings 1.2 — W3C