Most aria-labels on your buttons are dead weight, and some are bugs
An aria-label doesn't add to a button's accessible name, it replaces it. Here's why most of the ones you sprinkle on buttons are redundant, and how a few quietly break voice control and fail WCAG.
I was reviewing a diff from a Claude agent the other day. It had renamed a button from “Learn more” to “See scheduled freeze reference”, which was a fine change. Then on the same button it added aria-label="See scheduled freeze reference", the exact string the button already displayed. I told it to delete the attribute, which it did without argument.
That reflex is everywhere, and it’s worth pulling apart, because most of the time the aria-label you sprinkle on a button does nothing, and every so often it quietly breaks the thing it was meant to help.
Where the reflex comes from
The habit grows out of a belief: that without aria-* attributes, a screen reader is blind. Add labels everywhere and you’ve “done accessibility.”
A screen reader already reads the text of a link or a button. That’s the entire job. <button>See scheduled freeze reference</button> is announced as “See scheduled freeze reference, button” with nothing bolted on. You don’t owe it a translation. The visible text already is the accessible name.
I’ll be honest about why this belief survives on frontend teams: almost none of us have ever turned a screen reader on. I hadn’t really sat with one until this started bugging me. If you’ve never heard VoiceOver read your own UI, it’s easy to assume the worst and over-correct with attributes you never hear the effect of.
aria-label doesn’t add, it overwrites
Here’s the part that turns a harmless habit into a real problem. aria-label is not announced alongside the button text. It replaces it.
The browser computes exactly one accessible name per element, and there’s a strict priority order for it (the W3C spec calls it accname):
flowchart TD
A[Need the accessible name] --> B{aria-labelledby set?}
B -- yes --> B1[Use the referenced elements]
B -- no --> C{aria-label set?}
C -- yes --> C1[Use the aria-label string]
C -- no --> D{Has text content?}
D -- yes --> D1[Use the text content]
D -- no --> E{title attribute set?}
E -- yes --> E1[Use the title]
E -- no --> E2[No accessible name]
The moment you put an aria-label on an element, its own text content stops mattering to assistive tech. The label wins. When the label and the text say the same thing, you’ve duplicated a string for nothing. The real cost shows up later.
The drift
Strings change. Someone edits the visible button text and doesn’t think to touch the aria-label, because why would they? It’s an invisible attribute three lines up.
Take a button that started life saying “Learn more” with a matching aria-label="Learn more". Later someone makes the visible text actually useful and changes it to “See scheduled freeze reference”, and leaves the label untouched. Now the two disagree, and the invisible one wins:
- A screen reader user hears “Learn more” for a button that reads “See scheduled freeze reference”. They’re navigating against audio that no longer matches what’s on screen.
- A voice control user (Dragon, macOS Voice Control) looks at the button, says “click See scheduled freeze reference”, and nothing happens. Voice commands match the accessible name, which is still “Learn more”. The button is now unclickable by voice for the exact people who depend on voice.
And this isn’t only a matter of taste. WCAG 2.5.3, “Label in Name” (level A), requires the accessible name to contain the visible text. The accessible name “Learn more” doesn’t contain “See scheduled freeze reference”, so the button fails it outright. The attribute you added to be accessible has become a documented accessibility failure. That part I find funny.
We do this too
This isn’t me pointing at one agent. Our own dashboard is full of it. Two I found in about five minutes.
The sort arrows in our table headers carry labels that describe the icon:
<ArrowUp aria-label="arrow-up" ... />
<ArrowDown aria-label="arrow-down" ... />
That tells a screen reader user the shape of the glyph, not what the control does. They don’t care that there’s an arrow pointing up. They want “Sort by name, ascending.” We labeled the picture instead of the action.
The same “more options” menu also shows up as aria-label="more" in one file and aria-label="More options" in another. Two spellings for one control. Small thing on its own, but it’s a tell: nobody is hearing these read out loud, so nobody notices they’ve drifted apart.
The rule I use now
There’s an old line that the first rule of ARIA is don’t use ARIA. This is that line aimed at the one attribute people reach for most.
If a control has visible text, it doesn’t need an aria-label. The text is the name, and it stays in sync for free because it’s literally the same string the user reads.
If a control is icon-only, it does need a name, and that’s what aria-label is for. Even then I reach for a visually hidden <span> first. It lives in the DOM as real text, it shows up plainly in the JSX, and a future editor can actually see it instead of discovering it through a screen reader they’ll never run.
The one case worth nuance is text that’s too thin to stand alone, like a column of “Edit” buttons in a table. Don’t overwrite the visible word with a label. Keep it and extend it: an accessible name of “Edit rule #3” beats a label that throws the word “Edit” away and replaces it with something the user can’t see.
Go listen to one
Turn on VoiceOver (Cmd+F5 on a Mac) and tab through a page you built. It takes ten minutes and it rewires how you think about every one of these attributes. You’ll come away adding fewer of them, and the ones you keep will be the ones that earn their place.