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. One that is only spaces is skipped- Native host-language label:
<label for="…">or ancestor<label>for form controls<fieldset><legend>for grouped controls<caption>for tablesaltfor images and<input type="image">, the first<title>for an<svg>,titlefor iframes
- Text content — inner text, including text inside descendants. A descendant with a name of its own lends that name instead of its text: an image its
alt, an<svg>its<title>, any element itsaria-label. A<select>'s<option>is the exception, named by its text alone. Only for roles named from their content (buttons, links, headings, cells, options…): a dialog, image, landmark or text field is named by its author alone, never by its text 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.
One thing a tree shows beyond this list. A paragraph, a list item, or a live region (alert, status) with text directly inside it shows that text as its name: paragraph "Read the guide", status "3 results". That keeps its content visible in a snapshot, where the tree prints names and nothing else. A screen reader reads that text as the element's content, not as a label. It never applies to a dialog, image, landmark or text field.
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>Link with only an image
<a href="/">
<img src="/logo.svg" alt="Acme home" />
</a>→ "Acme home" (from #4). The link is named from its content, and what the image contributes is its alt. Any child with a name of its own lends that name rather than its text, so <button><span aria-label="Close">×</span></button> is "Close", not "×".
Dialog with text but no label
<div role="dialog">
Delete this project?
<button>Cancel</button>
</div>→ "" — no name. A dialog's text is its content, not its label: a screen reader announces this dialog unnamed. The same holds for an image, a landmark (<nav>, <aside>, <footer>…) and a text field, whose text is the user's value. (An unnamed <form> is not a landmark at all — it is a generic, and a generic does take its loose text.) This fails assertDialogsLabeled(). Fix it by pointing aria-labelledby at a visible heading:
<div role="dialog" aria-labelledby="delete-title">
<h2 id="delete-title">Delete this project?</h2>
<button>Cancel</button>
</div>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)".
A referenced element with an aria-label of its own lends that instead of its text, and so does an image with its alt. aria-describedby reads its targets the same way.
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. As in Chromium, any value butfalse, an empty value orundefinedhides, in any case:aria-hidden="TRUE"andaria-hidden="yes"hide too. - CSS-generated content (
::before,::aftercontent) — spec says it should contribute, but engine support is inconsistent. Don't rely on it for critical labels. - What is inside an image — an
<img>, an<svg>or arole="img"element lends its name, never its content.<button><span role="img">★</span></button>has no name, nor does a button whose only content is SVG<text>. Give the image anaria-label, or the button one. <img alt="">with empty alt — the image is treated as decorative and contributes no name. Unless it also has atitle: HTML-AAM keeps such an image in the tree and names it from the title, so<img alt="" title="Company logo">is animgnamed"Company logo".
Debugging an unexpected name
When treeSnapshot() 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.
Spacing between a name's parts
When a name is built from an element's content, each child that renders as a box of its own is separated with a space, and a child that flows inline is not — the same rule Chromium applies. So <button><div>Save</div><div>now</div></button> is "Save now", while <button><span>Sa</span><span>ve</span></button> is one word, "Save". A child that lends no text still separates the text around it if it renders: <h1>Save<input>now</h1> is "Save now".
Text a child lends from an attribute is spaced off even when the child flows inline: an image's alt, a child's aria-label, a nested link's aria-label. So <a href="/kb"><img alt="Help">center</a> is "Help center", as in Chromium. The rule is about the child itself, not what lies inside it: a link whose only content is an image is still named by its content, so <h2>Go to<a href="/"><img alt="Home"></a>now</h2> is "Go toHomenow".
Because the rule reads computed display, the same markup can produce a different name in jsdom than in a real browser wherever jsdom's CSS engine differs from a browser's. jsdom does not blockify flex or grid items, floats or absolutely positioned children, and it treats <select> and <textarea> as inline where a browser makes them inline-block. Those cases read as one word under a jsdom-based matcher and spaced in a browser. If a name looks glued only in jsdom, check the child's computed display before treating it as a bug — and prefer the Playwright path, which runs in a real browser, when the distinction matters to your assertion.
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 (treeSnapshot) 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.