Visual hiding and semantic hiding are different operations
Aliases: visually hidden · aria-hidden · inert · sr-only · clipped off-screen
What it is
Taking a region off the screen and taking it out of assistive technology are two independent switches. Visual hiding changes paint: clip, one-pixel shrink, transparency, off-viewport shift. The node stays in the accessibility tree and its name can still be computed. Semantic hiding changes exposure: display: none, visibility: hidden, hidden, aria-hidden, and inert remove the node from the tree or mark it unavailable. Invisible does not imply unspoken; unspoken does not imply invisible.
The verb "hide" collapses the two. For a screen-reader user they are opposite gifts: an sr-only label is meant only for them; aria-hidden="true" on a visible button takes away something sighted users still have.
Why it happens
Paint and the accessibility tree take different inputs. Opacity, clipping, and transform off-screen affect compositing; the platform API still receives the node, a screen reader will speak it, and Tab may still land on it unless focusability is turned off separately. display: none and hidden drop the node from both paint and the tree; visibility: hidden is typically unexposed; aria-hidden drops it from the tree while the pixels can remain; inert makes a whole subtree unfocusable and unclickable, which is semantic unavailability.
The switches are orthogonal, so they produce four outcomes: visible and spoken (ordinary controls), visible but unspoken (aria-hidden over visible UI), invisible but spoken (sr-only labels — the legitimate way to supply a name), invisible and unspoken (a truly closed panel). Failures are almost always the wrong pair: visually hiding an error so the screen reader never hears it; aria-hidden on a visible close button so keyboard focus still lands on a control that "does not exist".
Where it stops holding
Off-screen ads, captchas, or repeated decoration that should not be spoken will leak through visual hiding; those need semantic hiding or not rendering at all. Print CSS and opacity: 0 entrance motion still expose the node mid-animation; if the content is not ready to be read, a semantic switch is required — "temporarily unseen" is not "temporarily unread". Stacking inert or aria-hidden on a node that already holds focus can trap focus in a region assistive technology can no longer see; that is a focus problem hiding attributes cannot fix alone. Search engines and copy-paste do not use the accessibility tree; semantic hiding does not delete the text from the DOM.
Applying it
- Text that must become an accessible name without taking layout space uses paint-only sr-only / clip techniques, not
display: noneoraria-hidden. - UI that should be fully unavailable (the page behind a closed overlay, an inactive tab panel) uses semantic hiding or
inert, not a transparent scrim alone. - Never set
aria-hidden="true"on a control that is still visible and clickable. Never visually-only hide the text that errors, status, or names depend on. - How to check: for the same region, look at pixels and the accessibility tree. Name which of the four combinations you actually have. Then Tab through: unspoken nodes must not take focus; spoken-but-invisible nodes must be exactly the ones you meant to leave for assistive technology.