Skip to content
Alexandre Gaubert Alexandre Gaubert
June 22, 2026 · 6 min read

Most aria-labels on your buttons are dead weight, and some are bugs

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.

Stay ahead in CI/CD

Blog posts, release news, and automation tips straight in your inbox.

Recommended posts

We Spent Years Hardening Jinja2 in User Config. We're Removing It Instead.
May 20, 2026 · 5 min read

We Spent Years Hardening Jinja2 in User Config. We're Removing It Instead.

After years of patching a Jinja2 sandbox against hostile templates, we pulled every customer config, learned what users actually did with the feature, and replaced it with a narrow declarative schema.

Thomas Berdy Thomas Berdy
The Comfortable Room
March 6, 2026 · 5 min read

The Comfortable Room

Software engineering was a walled garden. AI just copied the key. The data is messy: 19% slower in trials, 30% more warnings, 322% more vulnerabilities. But the baseline wasn't pristine either. What's left isn't coding: it's judgment, taste, and knowing which room to build.

Rémy Duthu Rémy Duthu
Claude Didn’t Kill Craftsmanship
February 4, 2026 · 5 min read

Claude Didn’t Kill Craftsmanship

AI doesn't remove craftsmanship: it moves it. The goal was never to protect the purity of the saw. It's to build good furniture. Engineers can now focus on intent, judgment, and product quality instead of translating tickets into code.

Rémy Duthu Rémy Duthu