One string, and everything depends on it
Every control in the accessibility tree has an accessible name — the thing announced when focus lands on it, the thing listed when somebody asks for every button on the page, and the thing a voice-control user says to press it. A button with no name is announced as "button", which is unusable, and cannot be operated by voice at all.
The name is computed, not stored. The browser walks a defined order and takes the first thing it finds.
The order the browser resolves it in
Higher wins. aria-label="Save" on a button whose text says "Save the order" means the visible text is thrown away and the button is announced as "Save" — which is a bug and a very common one.
| # | Source | Example |
|---|---|---|
| 1 | aria-labelledby | points at other elements; their text is joined |
| 2 | aria-label | a string you write |
| 3 | the native label | <label>, alt, <legend>, <caption>, a button’s own text |
| 4 | title | a tooltip — a last resort, and invisible to touch |
| 5 | nothing | the control is nameless |
A voice-control user says what they see: "click Save the order". If the accessible name is "Save", nothing happens. WCAG has a criterion for exactly this — the name must contain the visible text.
The icon button
The second version is better than it looks: the text is real, so it survives a translation pass, and a voice-control user can say it. aria-hidden="true" on the icon stops the glyph being read as a word.
<button>✕</button> <!-- announced as "times" or "button" -->
<button aria-label="Close the quote">✕</button> <!-- good -->
<button>
<svg aria-hidden="true" focusable="false">…</svg> <!-- hide the decoration -->
<span class="visually-hidden">Close the quote</span>
</button> <!-- better: real text, hidden visually -->
Inputs: a label, not a placeholder
A placeholder does produce a name — and it disappears the moment somebody types, takes the person's memory as a dependency, is usually too low-contrast, and is not shown at all in some assistive settings. Use a real label and, if the design demands it, hide it visually rather than removing it.
<label for="email">Email address</label>
<input id="email" type="email" autocomplete="email"> <!-- best -->
<label>Email address <input type="email"></label> <!-- also fine -->
<input type="email" placeholder="Email address"> <!-- a name, and a bug -->
Hiding text from eyes, not from everyone
display: none and visibility: hidden remove the element from the accessibility tree as well as from the screen; aria-hidden="true" removes it from the tree only. This utility removes it from the screen only — which is what "for screen readers" means.
.visually-hidden {
position: absolute;
width: 1px; height: 1px;
margin: -1px; padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
Names that say nothing
- "Click here", "Read more", "Learn more" — a list of twenty links, all identical. The name should make sense read on its own: "Read the Vesper specification".
- The same name twice on the same page for two different things.
- A file name as alt text:
alt="IMG_4821.jpg". - "Button" in the name: it is already announced as a button. "Submit button" becomes "submit button button".
- A name that is only an emoji or a symbol, announced as its Unicode description.