skip to content

A `<button>` that visibly reads "Save" is announced by a screen reader as "Submit form", and voice-control users report that saying "click Save" does nothing. How would you find where that name came from, and why does the mismatch break voice control?

level: seniorimportance: should knowfreq 38%

answer

  1. inspect, do not infer
  2. the browser will tell you which source won
  3. an override is beating the visible text
  4. speech matches the announced string
  5. WCAG calls it Label in Name

basics

~20 s

Read the computed accessible name and its source in the browser's accessibility tree — DevTools names the winning source directly, and it will be an aria-label or aria-labelledby outranking the visible text. Voice control matches spoken commands against that name, so a name that omits the visible label leaves nothing to say.

solid answer

~50 s

Do not reason from the markup — inspect the accessibility tree. Chrome DevTools' Accessibility pane in the Elements panel and Firefox's Accessibility Inspector both show the element's computed name **and** which source produced it, which turns the question into one glance. Here it will be an `aria-label` or `aria-labelledby` on the button, because both outrank the element's own text content in the name computation, so the visible word "Save" is never announced. The voice-control failure is the same fact seen from another angle: speech-recognition tools match a spoken phrase against the accessible name, so a control whose name does not contain its visible label has no phrase a user can guess. That is exactly what WCAG success criterion 2.5.3, Label in Name, requires — the accessible name must contain the visible label text. The fix is to remove the override or rewrite it so it starts with "Save".

go deeper

for a junior

Know where to look: the browser's accessibility inspector shows the computed name, and an aria-label on a button that already has text will replace what the user sees.

for a middle

Explain the precedence that lets the override win, and state the Label in Name requirement that a control's accessible name must contain its visible label text.

for a senior

Demonstrate the full diagnosis: inspect the tree, identify the winning source, explain the voice-control consequence, and fix it by subtraction rather than by adding more ARIA.

for a principal

Own the prevention: decide whether shared components accept labelling props at all, and require role-plus-name assertions in tests so a copy edit that desyncs the announced name fails the build instead of reaching users.

## Start from the tree, not the markup The instinct is to read the JSX or the template harder. Do not — the accessible name is a computed value, and browsers will tell you the result and its provenance. In Chrome, select the element in the Elements panel and open the **Accessibility** pane: it shows the computed **Name** with the source that won, the **Description**, the role, and the node's position in the accessibility tree. Firefox's **Accessibility Inspector** presents the same information as a browsable tree. Both remove all guesswork about precedence, hidden nodes and generated content in a single step. Command-line and CI equivalents exist too. axe-based tooling reports name-related violations directly, and a test query like `getByRole('button', { name: /save/i })` — the pattern used by Testing Library and Playwright — fails precisely when the computed name stops containing the expected text, which is the automated form of this bug. ## Why the visible text lost The name computation stops at the first source that yields text, and the order puts author overrides on top: `aria-labelledby`, then `aria-label`, then the element's native source (subtree text for a button), then `title` as a fallback. So both of these produce a name that contradicts the screen: ```html <button aria-label="Submit form">Save</button> <span id="lbl">Submit form</span> <button aria-labelledby="lbl">Save</button> ``` In each case the DevTools pane will read something like `Name: "Submit form"` with the source shown as the overriding attribute, and the subtree text listed as an unused, lower-priority candidate. These rarely appear in one commit. A shared button component forwards a label prop; a consumer passes a generic string; the visible children change later during a copy edit and nobody updates the prop. The override survives the rename because nothing visual depends on it. ## Why voice control breaks Speech-recognition tools — Voice Control on Apple platforms, Voice Access on Android, Dragon on Windows — let a user activate a control by saying its label. They resolve the phrase against the **accessible name**, not against the pixels. When the name is "Submit form", saying "click Save" matches nothing, and the user is looking straight at a button labelled Save. They have no way to discover the hidden string; their fallback is to turn on a numbered-overlay mode and work around your page. Speech-input users are affected worst, but they are not the only ones. Sighted screen-reader users — including people with low vision or dyslexia who use speech alongside the visual page — hear one word and read another, which is disorienting. Support conversations break too, because "press Save" means different things to the person on each end. ## The rule this violates WCAG 2.1 added success criterion **2.5.3 Label in Name** (Level A) for exactly this: for controls with a visible text label, the accessible name must **contain** the text of that visible label. Note *contain*, not *equal* — extra context is allowed, so `aria-label="Save draft to account"` on a button reading "Save" passes, and best practice is to lead with the visible text so speech matching is predictable. Replacing the visible words with entirely different ones fails. ## Fixing and preventing The fix is nearly always subtraction: delete the `aria-label` and let the button's own text name it. An override earns its place only when the visible text is genuinely insufficient — for example, ten "Edit" buttons in a table where each needs to say which row it edits — and then it must still start with the visible word: `aria-label="Edit invoice 42"`, not `aria-label="Invoice 42 row actions"`. Preventing the recurrence is a component-API question. Options that actually work: - Do not let shared primitives forward a labelling prop unless the component genuinely renders no text. - Treat `aria-label` on an element with visible text as something a review must justify; linting can flag the shape, but the judgement about whether the strings agree is human. - Assert names in tests. `getByRole('button', { name: 'Save' })` in the component's own test binds the announced name to the visible label, so the next copy change breaks a test instead of a user. ## The habit The general lesson is broader than this one bug: the accessibility tree is an inspectable output of your markup, like computed styles. Any time the announced behaviour surprises you — a missing name, a doubled name, a stray glyph, a control announced as the wrong role — the first move is to open the tree and read what the browser actually computed, before forming any theory about why.

  • Does WCAG 2.5.3 Label in Name require the accessible name to be identical to the visible label?
    No — it requires the name to **contain** the visible label's text. Extra context is allowed and often useful, as in `aria-label="Edit invoice 42"` on a button reading "Edit". Best practice is to start the name with the visible words so speech matching is predictable, since some tools favour prefix matches. Replacing the visible text with different wording is what fails.
  • Ten buttons in a table all read "Edit". How do you disambiguate them without breaking Label in Name?
    Extend the name rather than replace it: give each button a name that begins with the visible word and adds the row's identity, such as "Edit invoice 42". Voice users can still say "click Edit" and get a disambiguation prompt, screen-reader users navigating by element list can tell the ten apart, and the visible column stays uncluttered.
  • How would you stop this regression from recurring in a design system?
    Make the announced name testable and hard to override casually. Assert it in component tests with a role-plus-name query so a copy change that desyncs the label fails CI, and reconsider whether shared primitives should forward a labelling prop at all when they already render text children. Automated checks catch nameless controls; only review catches a name that is present but wrong.

saying these in an interview costs you the question

  • Reads the markup instead of the accessibility tree
  • Assumes visible text always wins over aria-label
  • Thinks voice control matches the on-screen pixels
  • Adds aria-label defensively to already-labelled buttons
  • Believes an automated scanner would have caught it

context