ARIA labels are the most misunderstood part of web accessibility. Developers add them everywhere "just to be safe," or skip them entirely thinking HTML semantics are sufficient. Neither approach is correct. This guide covers what ARIA labels are, when each type is appropriate, and the patterns that actually work for assistive technologies.
What ARIA Labels Are For
ARIA (Accessible Rich Internet Applications) labels provide a text alternative that screen readers announce when a user focuses on an element. They don't change visual appearance — they only affect the accessibility tree that assistive technologies read.
The key rule: ARIA supplements HTML semantics. It doesn't replace them. If a native HTML element exists for your use case, use it. ARIA is for cases where HTML semantics are insufficient — custom widgets, complex interactive components, or cases where the visual label isn't accessible to screen readers.
The first rule of ARIA: no ARIA is better than bad ARIA. A broken ARIA implementation actively harms screen reader users. Untouched HTML with native semantics is a safer baseline than incorrect ARIA attributes.
The Four ARIA Labeling Mechanisms
1. aria-label
Provides a string label directly on the element. Overrides all other labeling mechanisms.
<!-- Icon-only button with no visible text -->
<button aria-label="Close dialog">
<svg aria-hidden="true">...</svg>
</button>
<!-- Search input without a visible <label> -->
<input type="search" aria-label="Search products" />
Use when: There's no visible text element that can serve as the label — typically icon-only controls, or inputs where the design intentionally omits a <label>.
Don't use when: A visible label exists. Use aria-labelledby instead (connecting screen reader output to the visible text keeps them in sync automatically).
2. aria-labelledby
References one or more existing elements by ID. The screen reader announces the concatenated text content of those elements.
<!-- Dialog title labels the dialog itself -->
<div role="dialog" aria-labelledby="dialog-title">
<h2 id="dialog-title">Confirm deletion</h2>
<p>This action cannot be undone.</p>
</div>
<!-- Multiple elements form the label -->
<div id="product-name">Running Shoes</div>
<div id="product-size">Size 10</div>
<button aria-labelledby="product-name product-size">Add to cart</button>
<!-- Screen reader announces: "Running Shoes Size 10, button" -->
Use when: A visible label element exists in the DOM. This is the preferred pattern over aria-label because the accessible name stays synchronized with the visible text.
Multiple IDs: Space-separate them. The text content of all referenced elements is concatenated in order.
3. aria-describedby
Like aria-labelledby, but provides a description rather than a label. The label identifies what an element is; the description provides additional context announced after the label.
<label for="password">Password</label>
<input
type="password"
id="password"
aria-describedby="password-requirements"
/>
<p id="password-requirements">
Must be 8+ characters and include one number.
</p>
<!-- Screen reader: "Password, edit text. Must be 8+ characters and include one number." -->
Use when: Additional instructions, error messages, or contextual hints supplement the primary label.
Error messages:
<input
type="email"
id="email"
aria-invalid="true"
aria-describedby="email-error"
/>
<p id="email-error" role="alert">
Enter a valid email address.
</p>
4. Native <label> element
For form inputs, a properly associated <label> is always preferred over ARIA:
<!-- Explicit association via for/id -->
<label for="username">Username</label>
<input type="text" id="username" />
<!-- Implicit association (wrapping) -->
<label>
Username
<input type="text" />
</label>
A <label> does two things ARIA can't: it provides an accessible name AND makes the label clickable to focus the input (improving usability for everyone). Use aria-label or aria-labelledby on inputs only when a visible <label> is genuinely not feasible.
Common ARIA Patterns
Accessible icon buttons
The most common ARIA use case. Icon-only buttons have no visible text, so they need aria-label:
<!-- Good -->
<button type="button" aria-label="Open menu">
<svg aria-hidden="true" focusable="false">...</svg>
</button>
<!-- Also good if you have a visible tooltip span -->
<button type="button" aria-labelledby="menu-label">
<svg aria-hidden="true" focusable="false">...</svg>
<span id="menu-label" class="sr-only">Open menu</span>
</button>
Always add aria-hidden="true" and focusable="false" to inline SVGs in buttons. Without these, some screen readers read the SVG title or try to navigate into the SVG paths.
Navigation landmarks
<nav aria-label="Main navigation">...</nav>
<nav aria-label="Breadcrumb">...</nav>
If a page has multiple <nav> elements, each needs a unique aria-label so screen reader users can distinguish them in the landmarks list.
Modals and dialogs
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-desc"
>
<h2 id="modal-title">Delete account</h2>
<p id="modal-desc">
This permanently removes your data. This action cannot be undone.
</p>
<button>Cancel</button>
<button>Delete</button>
</div>
aria-modal="true" tells screen readers that content outside the dialog is inert. You must also implement actual focus trapping in JavaScript — ARIA alone doesn't do it.
Live regions
For dynamic content updates (notifications, search results, loading states):
<!-- Polite: waits for current announcement to finish -->
<div aria-live="polite" aria-atomic="true" id="status">
3 results found
</div>
<!-- Assertive: interrupts — use sparingly for truly urgent updates -->
<div aria-live="assertive" role="alert">
Session expired. Please log in again.
</div>
aria-atomic="true" means the entire region is re-announced when any part changes, rather than just the changed portion.
Frequently Made Mistakes
Adding aria-label to elements that already have a label
<!-- Bad: redundant aria-label conflicts with visible label -->
<label for="email">Email address</label>
<input type="email" id="email" aria-label="Email" />
<!-- Good: let the <label> do its job -->
<label for="email">Email address</label>
<input type="email" id="email" />
Labeling non-interactive elements unnecessarily
<!-- Bad: <p> is not interactive, doesn't need a label -->
<p aria-label="Introduction paragraph">Welcome to our site.</p>
<!-- Good: just write the paragraph -->
<p>Welcome to our site.</p>
ARIA labels on static text elements like <p>, <div>, or <span> are usually ignored or cause confusion. Labels are for interactive elements and structural landmarks.
Generic labels that don't distinguish actions
<!-- Bad: two "Edit" buttons with no context -->
<button aria-label="Edit">Edit</button>
<button aria-label="Edit">Edit</button>
<!-- Good: distinguish by what is being edited -->
<button aria-label="Edit billing address">Edit</button>
<button aria-label="Edit shipping address">Edit</button>
Screen reader users often pull up a list of all interactive elements on a page. Identical labels make navigation impossible.
Using role="button" on a <div> instead of a <button>
<!-- Bad: requires manual keyboard handling and ARIA roles -->
<div role="button" aria-label="Submit" tabindex="0">Submit</div>
<!-- Good: native button has all this built in -->
<button type="submit">Submit</button>
Native <button> handles focus, keyboard activation (Enter and Space), and semantics automatically. Reserve ARIA roles for genuinely custom components.
Testing Your ARIA Implementation
Screen reader testing is non-negotiable. Computed accessibility trees and automated tools miss real-world behavior:
- macOS VoiceOver: Cmd+F5 to toggle. Use the Web Rotor (Ctrl+Option+U) to list headings, links, landmarks, and form controls.
- NVDA (Windows, free): Common screen reader for Windows testing. Use Browse Mode (arrows to navigate) vs Focus Mode (Tab for interactive elements).
- JAWS (Windows): Most common enterprise screen reader. Test critical flows if your users are likely on enterprise machines.
Automated checks:
- axe DevTools browser extension — flags many common ARIA errors
eslint-plugin-jsx-a11y— catches ARIA issues in JSX at write time
Browser accessibility panel:
- Chrome: DevTools → Elements → Accessibility tab — shows the computed accessible name and role for any element
- Firefox: DevTools → Accessibility panel — lists all elements with their ARIA properties
Quick Reference
| Attribute | What It Does | Best For |
|---|---|---|
aria-label |
Sets accessible name directly | Icon buttons, unlabeled inputs |
aria-labelledby |
Accessible name from existing element | Dialogs, sections, complex widgets |
aria-describedby |
Adds description after label | Error messages, instructions, hints |
aria-hidden="true" |
Hides from accessibility tree | Decorative icons, duplicated text |
aria-live="polite" |
Announces dynamic updates | Status messages, search results |
aria-live="assertive" |
Interrupts to announce | Errors, urgent alerts |
aria-expanded |
Toggle open/close state | Accordions, dropdowns, menus |
aria-invalid="true" |
Marks field as invalid | Form validation errors |
aria-required="true" |
Marks field as required | When not using required attribute |
The required HTML attribute is always preferred over aria-required for form fields — it does the same thing plus triggers native browser validation.