Accessibility · ARIA Practice

Hiding Subtrees Without Breaking Focus or the Tree

display none, visibility hidden, aria-hidden, and the hidden attribute remove content from different layers. Choosing wrong leaves focusable nodes inside invisible subtrees or visible text absent from the tree.

A menu that closes by sliding off screen keeps its items focusable, so the next Tab press lands on a link nobody can see. A tooltip hidden with aria-hidden stays visible to sighted users and vanishes from the accessibility tree, so the one group who needs it described gets nothing. A collapsed accordion panel removed with display: none loses the focus that was inside it, and focus falls to the document body. Several mechanisms are in common use for “hide this”, and they operate on different layers: layout, paint, hit testing, the accessibility tree, and the focus sequence. Any pair of them can disagree, and every one of these bugs is a disagreement between two of those layers.

The layers a subtree can occupy

An element participates independently in several systems, and hiding is a statement about which ones to remove it from.

Layout: whether the element generates boxes that occupy space. Paint: whether those boxes are drawn. Hit testing: whether pointer events reach it. The accessibility tree: whether assistive technology can find it. The focus sequence: whether Tab stops on it.

display: none removes the element from all five. No box, no paint, no pointer target, no tree node, no tab stop. This is the only mechanism that is total, and it is the correct default for content that is genuinely absent.

visibility: hidden removes paint and hit testing but keeps the box in layout. The element still occupies its space, so hiding it does not reflow the page. Crucially, it also removes the element from the accessibility tree, because browsers exclude invisible nodes, but it does not remove descendants that override the property. A child with visibility: visible inside a visibility: hidden parent is painted and is in the tree. That override is the mechanism behind the visually hidden pattern, and it is also a source of surprises: a visibility: hidden container with one visible child leaves a hole in the tree exactly where the invisible siblings were.

The hidden attribute maps to display: none through the user-agent stylesheet, with one exception worth knowing: the attribute is defined as applying display: none unless the element’s own display declaration overrides it. A stylesheet rule setting display: flex on [hidden] elements defeats the attribute entirely, which is a common accident in component libraries that style all their containers. When the attribute is defeated this way, it hides nothing at all, including from the tree.

aria-hidden="true" removes the element and its subtree from the accessibility tree and does nothing else. The element is still laid out, still painted, still clickable, and still focusable. It is a statement to assistive technology only, and it is not transitive across the tree in the way authors expect: it does apply to descendants, but a descendant that is focusable remains focusable, and browsers differ in how they resolve a focusable node inside an aria-hidden subtree. The specification explicitly forbids using aria-hidden on a focusable element, and the reason is that the resulting state has no coherent platform mapping.

The focusable node inside a hidden subtree

This is the failure that produces the worst user experience, because it strands the user in content that does not exist for them.

Consider a dropdown panel that is hidden by translating it outside the viewport or by clipping it to zero height, with the intention of animating it back in. Its links are still in the document, still have tabindex implicitly zero, and are still focusable. Pressing Tab moves focus into the panel. The browser scrolls to bring the focused element into view, which may scroll the page horizontally or vertically to a region that shows nothing. The screen reader announces a link whose name is real; a sighted keyboard user sees the viewport jump. Neither can see where they are.

The mechanism for preventing this is inert, which removes a subtree from hit testing, from the focus sequence, and from the accessibility tree in one declaration. It is a boolean attribute with no value, supported in current browsers, and it is the right tool for content that is present in the DOM for animation or state reasons but must not be reachable. The alternative is to remove focusability explicitly by writing tabindex="-1" on every interactive descendant, which requires the author to enumerate them and to keep the enumeration correct as the panel’s content changes.

The second-order problem is the focus that is already inside a subtree when it becomes hidden. If focus is on a button inside a panel and the panel gets display: none, the browser removes the node, and focus moves to the nearest ancestor that still exists, or to the document body. The user’s position is lost and the screen reader announces nothing in particular. A widget that hides a panel must move focus first, to the element that controls the panel, which is also where the user expects to be after closing it, and only then hide. The ordering is not a stylistic preference: hiding first destroys the node that focus would have to be moved from, and moving focus to a removed node is a no-op.

aria-hidden as a repair, not a hiding tool

aria-hidden earns its place in a narrower role: removing decorative or duplicated content from the tree while leaving it visible.

An icon font glyph next to a text label is the canonical case. The glyph is a character in a font, so it is text in the tree, and a screen reader may read the private-use codepoint as “unknown” or as whatever the font’s glyph name maps to. Marking the glyph’s element aria-hidden="true" leaves the visible label as the only thing announced. The same applies to a decorative SVG that duplicates adjacent text, and to a set of visual separators.

The related but distinct case is duplicated content that must remain for a different modality. A chart’s SVG carries no useful text; its data table equivalent is visually hidden. The SVG gets aria-hidden="true" and the table is exposed, or the reverse, depending on which is the primary representation. What must not happen is both being exposed, which produces a description followed by the same description.

The failure mode of aria-hidden is using it as the general-purpose hide. Because it leaves the element interactive, every use on anything with a click handler, a link, or an input creates the stranded-focus state. It also cannot be applied to the focused element at all: hiding the node that currently has focus leaves the tree with a focus pointer to nothing, and browsers resolve this inconsistently, with some moving focus and some continuing to report the hidden node as focused. The condition to check before writing the attribute is whether the subtree contains anything focusable, and the answer for a menu, a dialog, or a form panel is always yes.

Visually hidden, and the clipping technique

Content that must be available to assistive technology but not visible on screen needs the opposite of aria-hidden: removed from paint, retained in the tree. The conventional implementation clips the element to a one-pixel box rather than removing it.

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

Each declaration is load-bearing. position: absolute takes the box out of flow so the one-pixel size does not disturb layout. The negative margin and zero padding keep it from occupying a visible pixel. overflow: hidden prevents the text from spilling. clip-path handles the case where the element’s content is larger than its box. white-space: nowrap keeps the text on one line so it does not wrap into a tall invisible column that inflates the scroll height of the page.

The technique’s limits are worth stating. The element is still focusable, so a visually hidden link receives focus and shows no focus indicator, which is a defect if the link is meant to be operable. It is still in the tab order for the same reason. And screen readers running in a mode that uses the visual viewport as a cue, since some mobile readers allow touch exploration of visible content only, may not reach it. For labels and headings, which are not focusable, none of this bites. For interactive content, the pattern is a shortcut that should be replaced by a real visible control.

text-indent: -9999px is the older variant and it has a specific defect: an element with a negative text indent still occupies its normal width, so on a page with a scroll container it can extend the scrollable area, and in a right-to-left layout the direction of the overflow reverses. The clipping approach avoids both.

Interaction with the rest of the tree

Hiding a subtree changes what the tree contains, and anything that referenced the hidden nodes now references nothing.

aria-labelledby pointing at a hidden element still resolves. The name computation reads the referenced element’s text content, and the specification allows a hidden node to contribute a name, which is what makes the “label in a hidden element” pattern work. aria-describedby behaves the same way. aria-activedescendant does not: the referenced node must be in the accessibility tree, and a hidden active descendant leaves the container with a dangling reference. The same asymmetry applies to aria-controls, which is advisory and may point at hidden content without breaking.

Live regions are the other casualty, covered in how live regions turn DOM mutations into speech: a region hidden with display: none emits no events, because it has no node. A visually hidden region does emit them, because clipping does not remove the node. The distinction decides whether a status message is announced at all.

For modal content, hiding the background is the mechanism that makes a dialog modal in practice, and the containment question is treated in modal inertness and focus containment.

References

  1. WAI-ARIA 1.2: aria-hidden — W3C
  2. HTML Living Standard: The hidden attribute — WHATWG
  3. CSS Display Module Level 3 — W3C