a11ymedicaccessibility clinic

Guide

aria-label vs aria-labelledby: which one to use and when

Both attributes give an element its accessible name, the text a screen reader announces and a voice control user speaks. aria-labelledby points to visible text that is already on the page; aria-label holds a string that nobody sees. Choosing the right one, and knowing where neither works, avoids most of the ARIA naming bugs found in real audits.

By Alexandre Kohler, updated , 4 minute read

The short answer

  • If a native HTML way exists, use it first: a <label> for form fields, alt for images, text inside the button or link, <legend> for a fieldset, <caption> for a table.
  • If visible text on the page already names the element, use aria-labelledby and point to it. The name then matches what sighted users see and stays in sync when the text changes or is translated.
  • If there is no visible text at all, as with an icon only button, use aria-label.
  • If the extra text is a hint or a description rather than a name, use aria-describedby.

How the accessible name is computed

Browsers follow the W3C accessible name algorithm. Simplified, they take the first source that gives a non empty result, in this order:

  1. aria-labelledby: the text of the referenced elements, joined with spaces in the order of the ids.
  2. aria-label: the string value.
  3. The native label: a connected <label>, alt, <caption>, <legend>, and similar.
  4. The content of the element, for roles that are named from content, such as buttons, links and headings.
  5. Last resort: title and, for inputs, placeholder.

This order has an important consequence: aria-labelledby and aria-label override everything else, including the visible text inside a button. If a button shows "Buy now" and has aria-label="Add product 4821 to basket", screen readers announce only the aria-label. When both attributes are present, aria-labelledby wins. MDN has a detailed reference for aria-label and aria-labelledby.

When aria-label is the right choice

Use aria-label when the element has a clear visual meaning but no text: a close button with an X icon, a search button with a magnifier, a social link with only a logo, or a <nav> landmark that needs a name to tell it apart from a second navigation region.

aria-label examples
<!-- Icon only button -->
<button type="button" aria-label="Close">
  <svg aria-hidden="true" focusable="false" viewBox="0 0 24 24">...</svg>
</button>

<!-- Two navigation landmarks that need different names -->
<nav aria-label="Main">...</nav>
<nav aria-label="Breadcrumb">...</nav>

Keep in mind three drawbacks. The text is invisible, so designers and testers rarely notice when it goes out of date. Some automatic page translation tools do not translate attribute values, so a translated page may still announce English labels. And it is easy to write a name that does not match the visible label, which breaks voice control.

When aria-labelledby is the right choice

Use aria-labelledby when the name is already on screen. The classic cases are dialogs named by their heading, tab panels named by their tab, regions named by their section heading, and custom widgets named by a nearby text label. It takes one or more ids separated by spaces, and it can even reference an element that is hidden, which is useful for combining text.

aria-labelledby examples
<!-- Dialog named by its heading -->
<div role="dialog" aria-modal="true" aria-labelledby="delete-title">
  <h2 id="delete-title">Delete this file?</h2>
  ...
</div>

<!-- Unique link names built from visible text: "Read more Pricing update" -->
<h3 id="post-12">Pricing update</h3>
<a href="/blog/pricing" id="more-12" aria-labelledby="more-12 post-12">Read more</a>

In the second example ten "Read more" links stop being ten identical links in a screen reader's links list. The rule aria-dialog-name catches dialogs with no name, and when the dialog already has a heading with an id, a11ymedic offers the aria-labelledby fix automatically.

Name or description?

A name should be short: what the thing is or does. Extra information, like a password rule or an error message, belongs in a description. aria-describedby works like aria-labelledby (a list of ids) but its text is read after the name and role, often after a short pause, and it does not replace the name. "Password, edit text, at least 12 characters" is much easier to follow than a name that contains the full rule.

Where neither attribute works

Naming is only allowed on some roles. On a plain <div>, <span> or <p>, which have a generic or paragraph role, most screen readers ignore aria-label and aria-labelledby completely. The text you wrote is never heard. a11ymedic reports this as aria-prohibited-label. Put the text in the content (visible or visually hidden), or give the element a role that supports a name, such as role="img" for a graphic built from several elements or role="group" for a set of related controls.

Prohibited name and fixes
<!-- Before: ignored by most screen readers -->
<div aria-label="4 out of 5 stars"><span class="star"></span>...</div>

<!-- After: a role that can be named -->
<div role="img" aria-label="4 out of 5 stars"><span class="star"></span>...</div>

Common mistakes and how to catch them

  • Visible text missing from the name. A button shows "Search" but its aria-label is "Find products". Voice control users say "click Search" and nothing happens. This fails 2.5.3 Label in Name and is caught by label-in-name. Start the name with the visible text, or drop the aria-label.
  • Pointing to an id that does not exist. A typo in aria-labelledby gives an empty name. Duplicate ids are just as bad, because the browser uses the first match. The duplicate-id rule reports them.
  • Repeating the content. <button aria-label="Save">Save</button> adds nothing and is one more thing to keep in sync. Remove it.
  • Using aria-label to add instructions. That replaces the name. Use aria-describedby.
  • Naming hidden content. An element with aria-hidden="true" is removed from the accessibility tree no matter what label it has.

To see the name a browser would compute, use the developer tools accessibility panel, or call accessibleName from Node:

Inspect accessible names
import { parseHtml, accessibleName } from "a11ymedic";

const { document } = await parseHtml(
  `<h2 id="t">Delete file</h2>
   <div role="dialog" aria-labelledby="t"></div>
   <button aria-label="Close"><svg></svg></button>`
);

accessibleName(document.querySelector("[role=dialog]")); // "Delete file"
accessibleName(document.querySelector("button"));        // "Close"

For form fields specifically, see the accessible form labels guide. The full list of ARIA checks is in the rules reference.

Frequently asked questions

What is the difference between aria-label and aria-labelledby?

aria-label contains the name as a string that is not visible on the page. aria-labelledby points to the id of one or more visible elements whose text becomes the name. Prefer aria-labelledby when the text is already on screen.

Which wins if an element has both aria-label and aria-labelledby?

aria-labelledby. In the accessible name calculation it comes first, and both of them override the native label and the element's own text.

Can I use aria-label on a div?

Not on a plain div or span. Their generic role does not support a name, so most screen readers ignore it. Give the div a role that can be named, such as img or group, or put the text in the content.

When should I use aria-describedby instead of aria-label?

Use aria-describedby for extra information such as hints, formats and error messages. It is read after the name and does not replace it, while aria-label replaces the name entirely.