skip to content

In the accessible name computation, does `aria-labelledby` pointing at an element styled `display: none` still produce a name, and what is the accessible name of `<button>Delete <span aria-hidden="true">×</span></button>`?

level: seniorimportance: nice to knowfreq 30%

answer

  1. depends how the text was reached
  2. walking a subtree versus following a reference
  3. hidden descendants drop out
  4. direct references get an exception
  5. off-screen text is not hidden at all

basics

~20 s

Yes — a hidden element referenced directly by aria-labelledby still contributes its text, which is a deliberate exception. Inside the button's own subtree the opposite applies: an aria-hidden descendant is skipped, so the name is "Delete".

solid answer

~50 s

The computation has two different rules depending on how the text is reached. When it walks an element's own subtree, hidden nodes are skipped — `display: none`, `visibility: hidden`, the `hidden` attribute and `aria-hidden="true"` all remove a node from consideration — so `<button>Delete <span aria-hidden="true">×</span></button>` computes to "Delete", which is the standard trick for keeping a decorative glyph out of the announcement. But there is an explicit exception for nodes reached by a **direct** `aria-labelledby` or `aria-describedby` reference: those are used even when hidden, so `aria-labelledby` can point at a `display: none` element and still yield its text. It is specified behaviour rather than a quirk, though it is fragile in practice — relying on it means your label lives in markup nobody can see, so verify with a real screen reader before shipping it.

code

html · 12 lines
html
<!-- Decorative glyph excluded: name is "Delete" -->
<button>Delete <span aria-hidden="true">&times;</span></button>

<!-- Icon hidden, off-screen text supplies the name: "Delete invoice 42" -->
<button>
  <svg aria-hidden="true" viewBox="0 0 16 16" width="16" height="16"><path d="M2 4h12"/></svg>
  <span class="visually-hidden">Delete invoice 42</span>
</button>

<!-- Direct reference to a hidden node: still named "Delete invoice 42" -->
<span id="lbl" hidden>Delete invoice 42</span>
<button aria-labelledby="lbl">&#128465;</button>

go deeper

for a junior

Know the everyday half: aria-hidden="true" on a decorative icon keeps it out of the button's announcement, and hiding the text that names a control removes its name.

for a middle

Explain the two traversals — hidden nodes are skipped inside an element's subtree, but a node referenced directly by aria-labelledby is used even when hidden — and why off-screen text behaves differently from display: none.

for a senior

Show the shipping judgement: prefer the well-supported rendered-text path, treat naming from hidden content as something to verify with a real screen reader, and recognise a vanished name as a regression class that visual review cannot catch.

for a principal

Own the standard pattern for icon-and-label controls across the codebase so teams are not each choosing a different hiding technique, and make sure the accessible name is asserted somewhere automated rather than depending on individual care.

## Two traversals, two rules The accessible name computation reaches text in two different ways, and hidden content is treated differently in each. 1. **Subtree traversal** — walking an element's own descendants to build a name from its content, as for `<button>` or `<a>`. 2. **Reference resolution** — following `aria-labelledby` (or `aria-describedby`) to another element and taking its text. The default rule is that hidden nodes contribute nothing. The exception is that a node **directly referenced** by `aria-labelledby` or `aria-describedby` is included even when it is hidden. Everything below follows from those two sentences. ## What counts as hidden For this purpose, "hidden" is not only `display: none`. A node is hidden if it is not rendered or is explicitly removed from the accessibility tree: - `display: none` — not rendered at all - `visibility: hidden` — laid out but not painted - the `hidden` content attribute - `aria-hidden="true"` — rendered, but explicitly pruned from the accessibility tree All four exclude a node during subtree traversal. ## The subtree case ```html <button>Delete <span aria-hidden="true">×</span></button> <!-- accessible name: "Delete" --> ``` The traversal collects "Delete" from the text node and skips the span because `aria-hidden="true"` removes it. This is the idiomatic pattern for icon-plus-text buttons: the glyph or icon font is marked `aria-hidden` so users are not read a stray "times" or a private-use codepoint, while the real word supplies the name. The mirror-image mistake is marking the *text* hidden by accident. Collapse a label with `display: none` for a compact layout and the button's name goes with it — the control becomes an anonymous "button" with no announcement. ## The direct-reference exception ```html <span id="lbl" style="display: none">Delete invoice 42</span> <button aria-labelledby="lbl">🗑</button> <!-- accessible name: "Delete invoice 42" --> ``` By the spec, the span's text is used, because it is the node the attribute points at directly. The rationale is pragmatic: authors legitimately keep strings in the document that only assistive technology should consume, and a naming reference is an explicit statement of intent that outweighs the node's hidden state. The exception is narrow in an important way: it covers the referenced node itself, not arbitrary hidden descendants *inside* it. A referenced container whose own children are individually hidden does not automatically resurrect all of them. It is also the shakiest corner of the algorithm in real browsers and screen readers. Support for naming from `display: none` content has historically varied, so this pattern deserves a manual check with an actual screen reader rather than trust. It carries a maintenance cost too: a label that is invisible on screen is a label nobody notices when they change the button, and it cannot participate in "does the name match the visible text" review. ## Visually hidden is not hidden The distinction that trips people up: text moved off-screen or clipped by the common visually-hidden utility is still **rendered**, so it is not hidden for accessibility purposes and it participates normally in subtree traversal. ```html <button> <svg aria-hidden="true" viewBox="0 0 16 16"><path d="…"/></svg> <span class="visually-hidden">Delete invoice 42</span> </button> <!-- accessible name: "Delete invoice 42" --> ``` This is the robust version of the previous pattern and the one to prefer: it uses the normal, universally supported subtree path instead of the fragile hidden-reference exception, and the text is real content that survives copy edits and translation. The whole trick rests on the utility class using off-screen or clipping techniques rather than `display: none`, which would defeat it entirely. ## Generated content participates Text injected by `::before` and `::after` is part of the name computation, because it is rendered content in the subtree. That is a frequent source of mystery names — an icon font that injects a glyph through generated content can prepend a garbage character to a button's announcement unless the pseudo-element is kept out of the accessibility tree. ## How to work with it Prefer the paths with the fewest exceptions. Name a control from real, rendered text; hide decorative glyphs with `aria-hidden="true"`; use off-screen text when the label must not be visible; and reserve naming from `display: none` content for cases where nothing else fits — then confirm the result in the accessibility tree and with a screen reader. Chrome's Accessibility pane shows the computed name, so the answer to "did that hidden span count?" is always one glance away rather than a debate.

  • Why is off-screen text usually a better way to name an icon-only button than pointing `aria-labelledby` at a `display: none` element?
    Off-screen text is rendered, so it travels the ordinary subtree path that every browser and screen reader implements identically, while naming from `display: none` content relies on a narrow exception whose support has been uneven. It is also better maintenance: the string is real content that shows up in copy review and translation, instead of an invisible label nobody notices when the button changes.
  • What happens to a button's accessible name if a layout change sets `display: none` on the span holding its visible label?
    The name disappears. Subtree traversal skips hidden nodes, so with no other naming source the control computes to an empty name and announces as a bare "button". It is a silent regression — nothing looks broken visually, and only an accessibility-tree check or a role-plus-name test query catches it.
  • Can CSS generated content end up in an accessible name?
    Yes — text injected by `::before` or `::after` is rendered content inside the subtree, so the computation includes it. Icon fonts that inject a glyph this way can prepend a stray character to a button's announcement, which is why such pseudo-elements are normally kept out of the accessibility tree. It is a real cause of names that look inexplicable from the HTML alone.

saying these in an interview costs you the question

  • Thinks aria-hidden text still contributes to the name
  • Assumes hidden means hidden everywhere in the algorithm
  • Treats visually-hidden text as excluded from the name
  • Uses display:none labels without testing a screen reader
  • Believes aria-hidden on a focusable control is harmless

context