Accessible Names
The accessible name is the string a screen reader announces for an element. It's what your user actually hears — and it's almost never just the element's text content. This page explains how that name is computed, what contributes, and what wins when multiple sources compete.
Every SemanticNode.a11y.name in a Real A11y tree comes from this algorithm.
The priority order
For any element, the accessible name is picked from the first non-empty source in this list:
aria-labelledby— references another element's text contentaria-label— literal string on the element itself- Native host-language label:
<label for="…">or ancestor<label>for form controls<fieldset><legend>for grouped controls<caption>for tablesaltfor images,titlefor iframes
- Text content — inner text, including text inside descendants
titleattribute — last-resort fallback- Empty string (element has no accessible name)
If #1 resolves to a non-empty string, #2–#5 are ignored — even if #2 looks "more specific" from a CSS or developer perspective.
Concrete examples
Button with only text
<button>Save changes</button>→ "Save changes" (from #4, text content).
Button with an icon and hidden text
<button>
<svg aria-hidden="true">…</svg>
<span class="sr-only">Close dialog</span>
</button>→ "Close dialog" (from #4). The aria-hidden SVG contributes nothing; the visually-hidden <span> does because screen readers read it.
Button with only an icon
<button>
<svg aria-hidden="true">…</svg>
</button>→ "" — no name. This fails assertNoUnlabeledInteractive(). Fix with aria-label:
<button aria-label="Close dialog">
<svg aria-hidden="true">…</svg>
</button>Form control with a wrapping label
<label>
Email
<input type="email" />
</label>→ input's name is "Email" (from #3 — native label association). Real A11y's label handling prunes the label text from the tree so it doesn't surface as a redundant generic "Email" sibling.
Form control with for / id
<label for="q">Search</label>
<input id="q" type="search" />→ input's name is "Search" (same path).
aria-label beats text content
<a href="/home" aria-label="Go to homepage">Home</a>→ "Go to homepage" (from #2). The visible text "Home" is ignored by screen readers here — a classic footgun if the two drift apart.
aria-labelledby beats everything
<h2 id="section-title">Pricing plans</h2>
<section aria-labelledby="section-title" aria-label="ignored">
…
</section>→ region's name is "Pricing plans" (from #1). The aria-label is ignored.
aria-labelledby can reference multiple IDs — their text content is concatenated with spaces:
<label id="first">First name</label>
<span id="req">(required)</span>
<input aria-labelledby="first req" />→ input's name is "First name (required)".
What does not contribute
placeholder— by the spec, placeholders are a fallback name only if nothing else is available, and many screen readers ignore them entirely. Don't rely on placeholders as labels.title— the last fallback. It surfaces as a tooltip in desktop browsers, but many AT users (mobile, voice control) never see it. Treat it as documentation, not a label.- Text inside
aria-hidden="true"subtrees — excluded from name computation. - CSS-generated content (
::before,::aftercontent) — spec says it should contribute, but engine support is inconsistent. Don't rely on it for critical labels. <img alt="">with empty alt — the image is treated as decorative and contributes no name.
Debugging an unexpected name
When auditSnapshot() shows a name you didn't expect:
- Check for
aria-label/aria-labelledbyon the element or an ancestor — #1 and #2 override everything else. - Follow the
aria-labelledbychain. The referenced IDs might be missing, hidden, or pointing at the wrong element. - Look for visually-hidden text inside the element —
.sr-only,.visually-hidden. Often intentional, sometimes accidental. - Check
aria-hiddenon ancestors — a hidden wrapper removes the element's subtree from name computation. - Compare against the Chrome extension. If the extension shows the same name, it's what real AT will announce.
Why this matters
The accessible name is the only piece of text a screen reader user has to distinguish one element from another. Two buttons labeled "Edit" in a list of items are indistinguishable — not because the user is confused, but because from their perspective there is no more information.
Real A11y's assertions (assertNoUnlabeledInteractive, assertDialogsLabeled) and snapshots (auditSnapshot) all surface the accessible name as the primary identifier — because that's what the user hears, and that's what your tests should lock in.