Errors and fixes
aria-label on buttons: when it helps and when it breaks the accessible name
aria-label replaces the visible text instead of completing it. When the value omits the button text, voice control cannot press the command on screen.
Key points
- A button with visible text needs no aria-label: the accessible name is already the text.
- When you add aria-label, start the value with the visible text: “Save draft”, not “Submit form”.
- An icon-only button must have an aria-label that describes the action.
Short answer: a button with visible text already has its accessible name. Adding aria-label replaces that name, it does not complete it. If the value leaves out the visible text, people who use voice control cannot press the command they see.
The three forms of an accessible name
| Element | Accessible name | Correct use |
|---|---|---|
<button>Save</button> | “Save” | no extra attribute needed |
| icon-only button | none without help | aria-label="Close" |
| button with text and icon | the text | aria-hidden on the icon |
Why the name gets replaced
The reason sits in the order of computation. The accessible name is not an attribute; it is a computed value, and the Accessible Name and Description Computation specification lists the sources in sequence. First an author pointing elsewhere with aria-labelledby, then aria-label, then the content of the node. aria-label does not append to that list: it sits above the text and covers it. The visible text stays on screen and vanishes from the accessibility tree, which is why the value has to contain it.
WCAG 2.5.3, Label in Name, sets the rule: the accessible name must contain the visible text. It need not be identical, but it must start with the same words.
The usual mistake
// sbagliato: il nome accessibile non contiene «Salva»
Salva
// corretto: il testo visibile è già sufficiente
Salva
// corretto: icona decorativa nascosta, azione nel nome
In the second case the icon must not be announced. Without aria-hidden, an SVG with a title adds words to the name and the screen reader announces it twice.
When the label sits outside the button
Salva la bozza senza pubblicare
Salva
How to read it in ten seconds
// DevTools → Console: elenca i bottoni il cui nome non contiene il testo visibile
[...document.querySelectorAll("button")].forEach((b) => {
const label = b.getAttribute("aria-label");
if (!label) return;
const visible = (b.textContent || "").trim().toLowerCase();
if (visible && !label.toLowerCase().includes(visible)) console.warn(b, label);
});
The same check works without code: open the Elements panel, select the button and read Computed name in the Accessibility tab of Chrome or Firefox. When the line says from attribute, the name comes from the attribute rather than from the text. Comparing it with what the visitor sees takes a second.
aria-labelledby points at visible text, which beats aria-label: the name stays in the content instead of being duplicated in an attribute.
When the fix does not work
If the name the screen reader reads does not change, check precedence: aria-labelledby wins over aria-label, which wins over the content. An empty aria-label="" does not clear the name, while aria-label with a single space does. Avoid that case: it leaves the button unnamed. On icon-only buttons, title is not a reliable substitute.
When the usual fix makes things worse
The most common fix is to align the attribute with the wording: aria-label="Save draft" next to Save draft. It works today. The trouble is that the same sentence now lives in two places, the attribute and the content, and the translation file only touches the second. After the next copy revision the attribute stays in one language and the text does not: the computed name stops containing the visible text again, and nobody notices, because each string reads correctly on its own.
The limit of this rule
One limitation worth stating plainly: success criterion 2.5.3 covers only controls that carry a visible label. An icon-only button sits outside it, and no automated check tells you whether the reading order makes sense. axe and Lighthouse report a missing name, never a name that badly replaces the text, so a green test does not prove the command can be spoken.
How to prevent it
- A button with visible text gets no
aria-label. - Every decorative icon carries
aria-hidden="true". - In the test, read the accessible name in the Accessibility panel and compare it with the visible text.
- The words of the accessible name live in one place only: the visible content or
aria-labelledby, never copied into an attribute.
Where to continue
In the catalogue: login form, components and blocks. The guides: accessibility checklist and palette with measured contrast; the comparison: Radix, Base UI and React Aria.
Frequently asked questions
Why is aria-label risky on a button with text?
It replaces the accessible name. Someone who says “click Save” finds no match when the name is “Submit form”.
What do I use for an icon?
Hide a decorative icon with aria-hidden="true" and give the button an aria-label with the action, for example “Close”.
aria-label and aria-labelledby together?
No. When both are present, aria-labelledby wins and the aria-label value is ignored. Pick one.
Can I use title as a label?
No. The title is a hint, not a reliable accessible name: many screen readers ignore it.
How do I check the accessible name?
Open the Accessibility panel in browser devtools: it shows the computed name. If that name does not contain the visible text, something is wrong.