Accessibility · Semantics and the A11y Tree

The Order of Sources in Accessible Name Computation

How an accessible name is assembled from aria-labelledby, aria-label, host-language labels, and content, and where the recursion stops.

Every object in the accessibility tree can carry a name: a short string that identifies it when spoken, listed, or used as the target of a voice command. The algorithm that produces that string is specified step by step, and it is a cascade, not a merge. Earlier sources suppress later ones entirely. A button with aria-label="Close" and visible text “Dismiss” is announced as “Close”; the visible text is not appended, and the mismatch between what is seen and what is heard is the author’s problem to avoid.

The cascade runs roughly in this order: aria-labelledby, then aria-label, then host-language labelling mechanisms, then, for roles that permit it, the element’s own subtree. Each step can terminate the computation or defer to a traversal of other nodes; understanding where a step defers, and what it does with the nodes it visits, is most of the difficulty. The algorithm only runs for objects that already exist in the tree, which is why a node removed by the exposure rules described in How a Document Becomes an Accessibility Tree has no name to compute.

References come first, and they flatten

aria-labelledby holds a list of element IDs. The algorithm resolves them in document order, takes the name of each referenced element, joins the pieces with a single space, and stops. Two consequences follow directly.

The first is that a reference to an element which is itself labelled by aria-label yields that label, not its text. If <span id="a" aria-label="Alpha">first</span> is referenced, the contribution is “Alpha”. This makes aria-labelledby compose with other naming mechanisms, and it also makes it possible to build a name out of elements that are never visible.

The second is that a reference to an element containing an image yields the image’s name, which for a img alt="…" is the alt text. A name can therefore be assembled from a mixture of text, alt text, and other labels, with the pieces separated by spaces that the author cannot control. Joining is literal: a label built from <span>Save</span><span>draft</span> becomes “Save draft”, not “Savedraft” and not “Save, draft”.

Traversal of a referenced subtree skips anything hidden from the accessibility tree, with one exception: if the referenced element itself is hidden, its text is still used, because the author pointed at it deliberately. Descendants of that hidden element which are independently hidden are skipped. The exception exists so that visually hidden text (a span with clip-path and a one-pixel size, or display: none used purely as a label store) can serve as a name source without appearing in the reading order.

Cycles terminate. If A is labelled by B and B is labelled by A, the algorithm tracks the set of nodes already visited in the current computation and returns an empty contribution for a repeat. The result is not an error; it is a name that may be shorter than the author intended, and browsers do not report it.

<button aria-labelledby="qty-label qty-value">
  <span class="icon" aria-hidden="true">×</span>
</button>
<span id="qty-label">Quantity</span>
<span id="qty-value">3</span>
<!-- computed name: "Quantity 3" -->

Flat strings and the aria-label step

aria-label is a direct string. It is used when aria-labelledby is absent or resolves to nothing. Its value is the whole name: no trimming of internal whitespace beyond the usual collapsing of runs, and no concatenation with anything else. An empty aria-label="" is treated as no label at all rather than as an intentional empty name, which is why a decorative element cannot be silenced by giving it an empty label; aria-hidden or a presentational role is the mechanism for that.

A whitespace-only aria-label is likewise ignored. This is a small but real failure mode: templating engines that emit aria-label=" " when a variable is unset produce an element that falls through to its contents, so an icon button with no text becomes unnamed instead of being caught by a test that looks for an empty label.

Host-language labels

When neither ARIA attribute produces a name, the element’s own language supplies one. For a form control that is labelable, the associated label elements contribute, concatenated in tree order. The association is either explicit, through for pointing at the control’s ID, or implicit, through nesting the control inside the label. An implicit label’s text excludes the text of the control itself, so <label>Accept <input type="checkbox"></label> names the checkbox “Accept” and not “Accept Accept”.

alt on an image, title on an iframe, the value of a submit button, and the text content of a link all belong to this step. The title attribute is a late resort in general: for most elements it is consulted only after content has failed to produce a name, and its use as a tooltip is a separate mechanism from its use as a name. An element with both a title and visible content therefore takes the content, and the title becomes a description instead.

Placeholder text is not a label. It is exposed as a description in some mappings and as a name in none that follow the current specifications. A field whose only hint is placeholder has an empty name, which is why the pattern is treated as an error rather than a shortcut.

Content, when the role allows it

The final step walks the element’s own descendants and accumulates their text, but only for roles whose definition includes name from content. button, link, heading, cell, option, checkbox, radio, tab, and treeitem are in that set. textbox, combobox, listbox, group, region, img, and banner are not. The distinction is not stylistic: a text field’s value is what the user typed, and deriving a name from it would make the name change with every keystroke.

The traversal itself has rules that differ from a naive textContent. Hidden descendants are skipped. Elements with aria-label contribute their label rather than their text. Images contribute alt text. A descendant with role="presentation" or role="none" contributes its text but not its own semantics. Embedded controls inside a label contribute their value in some cases: a select inside a label contributes the selected option’s text.

Whitespace handling is the least portable part. The specification calls for a single space between the contributions of block-level children and no space between inline ones, with leading and trailing whitespace trimmed at the end. Engines approximate this from layout rather than from display values, which means a name computed from inline elements styled display: block can gain or lose a space relative to another engine. A name built by concatenating many spans with no separating text is the case most likely to produce “Savefile” in one browser and “Save file” in another.

Where the algorithm is expensive

Name computation is recursive and can visit a large subtree. A role="button" wrapping a table of two thousand cells forces the algorithm to walk every cell and concatenate their text. Because names are cached per object and invalidated on subtree change, a live region that mutates text inside such a button re-runs the walk on each mutation. The cost is proportional to the number of text nodes visited, and the resulting string can exceed any useful length; browsers do not truncate, but platform APIs and screen readers impose their own limits on what they will speak.

The interaction with the shadow DOM adds a boundary. A label in the light DOM cannot reference an ID inside a closed shadow root, and aria-labelledby does not cross shadow boundaries in the direction authors usually expect. Slots complicate the traversal further, because the flattened tree, not the DOM tree, is what gets walked. For elements assembled from custom elements, the name often ends up empty even though text is visible, and the fix is to label the host explicitly rather than to rely on content.

Descriptions follow a parallel but shorter cascade: aria-describedby, then aria-description where supported, then title, then, for some elements, nothing. The two attributes are frequently confused, and the practical difference is that a description is announced after a pause and is not used to identify the object in a list of objects. Which of the two a given pattern needs is a design question; the mechanism that carries both is described alongside roles in Mapping ARIA Roles onto Platform Accessibility APIs.

References

  1. Accessible Name and Description Computation 1.2 — W3C
  2. Accessible Rich Internet Applications (WAI-ARIA) 1.2 — W3C
  3. HTML Accessibility API Mappings 1.2 — W3C
  4. HTML Standard — The label element — WHATWG