Blog

ARIA Labels Guide: When and How to Use ARIA for Accessibility

TestParty
TestParty
March 29, 2025

ARIA (Accessible Rich Internet Applications) extends HTML's accessibility capabilities for complex web applications. When native HTML cannot convey meaning, relationships, or states to assistive technologies, ARIA fills the gaps.

However, ARIA is frequently misused. Incorrect ARIA is often worse than no ARIA—it can override correct native semantics and create barriers where none existed. The first rule of ARIA is: don't use ARIA if you can use native HTML.

This guide covers when ARIA is appropriate, how to use common ARIA attributes correctly, and patterns that cause harm.


What Is ARIA?

WAI-ARIA (Web Accessibility Initiative – Accessible Rich Internet Applications) is a W3C specification that defines attributes extending HTML semantics for accessibility.

How ARIA Works

ARIA attributes modify the accessibility tree—the browser's representation of page content for assistive technologies. ARIA changes what assistive technologies announce, but it doesn't change visual appearance or keyboard behavior.

ARIA provides:

Roles: Define what an element is (button, tab, alert) States: Define current condition (expanded, selected, disabled) Properties: Define characteristics (labelledby, describedby, controls)

What ARIA Cannot Do

ARIA is purely semantic—it communicates information but doesn't implement behavior:

  • ARIA cannot add keyboard functionality
  • ARIA cannot change visual appearance
  • ARIA cannot add focus capability
  • ARIA cannot make elements clickable

If you add role="button" to a <div>, you must also:

  • Add tabindex="0" for focus
  • Add Enter/Space key handlers
  • Add click handling
  • Add appropriate styling

The Rules of ARIA

The W3C establishes five rules for ARIA use.

Rule 1: Don't Use ARIA If You Can Use Native HTML

Native HTML elements have built-in accessibility. A <button> element is automatically focusable, keyboard operable, and announced as a button. ARIA cannot improve on native semantics.

<!-- Wrong: ARIA on div when button exists -->
<div role="button" tabindex="0" onclick="submit()">Submit</div>

<!-- Correct: Native button -->
<button onclick="submit()">Submit</button>

Rule 2: Don't Change Native Semantics

Don't override native semantics unless absolutely necessary.

<!-- Wrong: Tab semantics on a heading -->
<h2 role="tab">Section Title</h2>

<!-- Correct: Use appropriate element -->
<div role="tab">Section Title</div>

Rule 3: All Interactive ARIA Controls Must Be Keyboard Usable

Any element with an interactive role must support appropriate keyboard interaction.

<!-- Wrong: Button role without keyboard support -->
<span role="button" onclick="action()">Click me</span>

<!-- Correct: Button role with full keyboard support -->
<span role="button"
      tabindex="0"
      onclick="action()"
      onkeydown="if(event.key==='Enter'||event.key===' ')action()">
  Click me
</span>

<!-- Best: Just use a button -->
<button onclick="action()">Click me</button>

Rule 4: Don't Hide Focusable Elements

Don't use role="presentation" or aria-hidden="true" on focusable elements.

<!-- Wrong: Hidden but focusable -->
<button aria-hidden="true">Hidden Button</button>

<!-- Correct: Hidden and not focusable -->
<button aria-hidden="true" tabindex="-1">Hidden Button</button>

<!-- Or simply: -->
<button hidden>Hidden Button</button>

Rule 5: All Interactive Elements Must Have Accessible Names

Every interactive element needs an accessible name.

<!-- Wrong: No accessible name -->
<button><i class="icon-search"></i></button>

<!-- Correct: aria-label provides name -->
<button aria-label="Search">
  <i class="icon-search" aria-hidden="true"></i>
</button>

ARIA Labeling Attributes

Labeling attributes provide accessible names and descriptions.

aria-label

Provides a text label for elements without visible text.

Use when:

  • Icon-only buttons
  • Inputs without visible labels (use sparingly)
  • Elements where visible text is insufficient
<!-- Icon button -->
<button aria-label="Close dialog">
  <svg aria-hidden="true"><!-- close icon --></svg>
</button>

<!-- Navigation with generic label -->
<nav aria-label="Product categories">
  <!-- nav content -->
</nav>

<!-- Search input without visible label -->
<label for="search" class="visually-hidden">Search</label>
<input type="search" id="search" aria-label="Search products">

Don't use when:

  • Visible text already labels the element
  • aria-labelledby can reference existing text

aria-labelledby

References existing elements as the accessible name.

Use when:

  • Visible text already exists that should serve as the label
  • Multiple elements together form the label
  • You want to reference heading text
<!-- Label from heading -->
<section aria-labelledby="section-title">
  <h2 id="section-title">Featured Products</h2>
  <!-- section content -->
</section>

<!-- Multiple elements as label -->
<button aria-labelledby="item-name item-action">
  <span id="item-name">Blue Shirt</span>
  <span id="item-action">Add to Cart</span>
</button>
<!-- Announces: "Blue Shirt Add to Cart, button" -->

<!-- Dialog labeled by its heading -->
<div role="dialog" aria-labelledby="dialog-title">
  <h2 id="dialog-title">Confirm Purchase</h2>
  <!-- dialog content -->
</div>

Key behaviors:

  • aria-labelledby overrides other label sources
  • Can reference multiple IDs (space-separated)
  • Can reference hidden elements
  • Can reference the element itself

aria-describedby

Provides additional descriptive text beyond the name.

Use when:

  • Instructions or hints supplement form fields
  • Error messages describe issues
  • Additional context aids understanding
<!-- Form field with description -->
<label for="password">Password</label>
<input type="password"
       id="password"
       aria-describedby="password-requirements">
<p id="password-requirements">
  Must be at least 8 characters with one number
</p>
<!-- Announces: "Password, edit text. Must be at least 8 characters with one number" -->

<!-- Field with error message -->
<label for="email">Email</label>
<input type="email"
       id="email"
       aria-invalid="true"
       aria-describedby="email-error">
<p id="email-error">Please enter a valid email address</p>

<!-- Button with additional context -->
<button aria-describedby="delete-warning">Delete Account</button>
<p id="delete-warning" hidden>This action cannot be undone</p>

Difference from aria-labelledby:

  • aria-labelledby defines what the element IS (name)
  • aria-describedby provides additional information ABOUT the element (description)
  • Name is announced first, description follows

ARIA Roles

Roles define what an element represents to assistive technologies.

Landmark Roles

Define page structure regions:

<header role="banner">Site Header</header>
<nav role="navigation">Main Navigation</nav>
<main role="main">Primary Content</main>
<aside role="complementary">Sidebar</aside>
<footer role="contentinfo">Site Footer</footer>
<form role="search">Search Form</form>
<section role="region" aria-label="Featured">Featured Section</section>

Note: Semantic HTML elements (<header>, <nav>, <main>, etc.) automatically have these roles. Only add explicit roles when using non-semantic elements.

Widget Roles

Define interactive components:

| Role          | Purpose                     | Native Element Alternative |
|---------------|-----------------------------|----------------------------|
| `button`      | Clickable button            | `<button>`                 |
| `link`        | Navigation link             | `<a href>`                 |
| `checkbox`    | Toggle option               | `<input type="checkbox">`  |
| `radio`       | Single selection from group | `<input type="radio">`     |
| `textbox`     | Text input                  | `<input type="text">`      |
| `listbox`     | Selection list              | `<select>`                 |
| `slider`      | Range selector              | `<input type="range">`     |
| `switch`      | On/off toggle               | `<input type="checkbox">`  |
| `tab`         | Tab control                 | —                          |
| `tabpanel`    | Tab content                 | —                          |
| `tablist`     | Tab container               | —                          |
| `menu`        | Menu container              | —                          |
| `menuitem`    | Menu option                 | —                          |
| `dialog`      | Modal dialog                | `<dialog>`                 |
| `alertdialog` | Alert requiring response    | —                          |
| `tooltip`     | Tooltip content             | —                          |
| `progressbar` | Progress indicator          | `<progress>`               |

Composite Roles

Complex widgets composed of multiple elements:

<!-- Tab interface -->
<div role="tablist" aria-label="Product information">
  <button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
    Description
  </button>
  <button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2">
    Reviews
  </button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  Description content...
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  Reviews content...
</div>

<!-- Menu -->
<button aria-haspopup="menu" aria-expanded="false">
  Options
</button>
<ul role="menu">
  <li role="menuitem">Edit</li>
  <li role="menuitem">Delete</li>
  <li role="separator"></li>
  <li role="menuitem">Help</li>
</ul>

Live Region Roles

Announce dynamic content changes:

<!-- Alert for immediate announcements -->
<div role="alert">
  Form submitted successfully!
</div>

<!-- Status for polite announcements -->
<div role="status">
  3 items in cart
</div>

<!-- Log for chat/activity messages -->
<div role="log" aria-live="polite">
  <!-- New messages appear here -->
</div>

ARIA States and Properties

States and properties communicate element conditions.

Common States

| Attribute       | Values                                        | Use                                      |
|-----------------|-----------------------------------------------|------------------------------------------|
| `aria-expanded` | `true`/`false`                                | Accordion, dropdown, tree expanded state |
| `aria-selected` | `true`/`false`                                | Tab, option selection state              |
| `aria-checked`  | `true`/`false`/`mixed`                        | Checkbox, switch checked state           |
| `aria-pressed`  | `true`/`false`/`mixed`                        | Toggle button pressed state              |
| `aria-disabled` | `true`/`false`                                | Disabled state (prefer HTML `disabled`)  |
| `aria-hidden`   | `true`/`false`                                | Hidden from assistive tech               |
| `aria-invalid`  | `true`/`false`/`grammar`/`spelling`           | Validation state                         |
| `aria-busy`     | `true`/`false`                                | Loading/updating state                   |
| `aria-current`  | `page`/`step`/`location`/`date`/`time`/`true` | Current item indicator                   |

Implementation Examples

Expandable section:

<button aria-expanded="false" aria-controls="details">
  Show Details
</button>
<div id="details" hidden>
  Details content...
</div>

<script>
  button.addEventListener('click', () => {
    const expanded = button.getAttribute('aria-expanded') === 'true';
    button.setAttribute('aria-expanded', !expanded);
    details.hidden = expanded;
  });
</script>

Toggle button:

<button aria-pressed="false">
  Dark Mode
</button>

<script>
  button.addEventListener('click', () => {
    const pressed = button.getAttribute('aria-pressed') === 'true';
    button.setAttribute('aria-pressed', !pressed);
    // Update visual state
  });
</script>

Navigation current page:

<nav>
  <a href="/" aria-current="page">Home</a>
  <a href="/products">Products</a>
  <a href="/about">About</a>
</nav>

Common Properties

| Attribute              | Purpose                                                   |
|------------------------|-----------------------------------------------------------|
| `aria-controls`        | ID of controlled element                                  |
| `aria-owns`            | IDs of owned elements (for virtual relationships)         |
| `aria-haspopup`        | Indicates popup type (`menu`, `dialog`, `listbox`)        |
| `aria-modal`           | Indicates modal behavior                                  |
| `aria-required`        | Indicates required field                                  |
| `aria-readonly`        | Indicates read-only state                                 |
| `aria-multiselectable` | Allows multiple selection                                 |
| `aria-autocomplete`    | Describes autocomplete behavior                           |
| `aria-sort`            | Column sort direction (`ascending`, `descending`, `none`) |

Live Regions

Live regions announce dynamic content changes to screen readers.

aria-live

Defines how urgently changes should be announced:

<!-- Polite: Waits for pause in speech -->
<div aria-live="polite">
  <!-- Non-urgent updates -->
</div>

<!-- Assertive: Interrupts current speech -->
<div aria-live="assertive">
  <!-- Urgent updates (use sparingly) -->
</div>

<!-- Off: No announcements (default) -->
<div aria-live="off">
  <!-- Silent updates -->
</div>

aria-atomic

Whether to announce entire region or just changes:

<!-- Announce entire region on any change -->
<div aria-live="polite" aria-atomic="true">
  Cart total: $<span>45.00</span>
</div>
<!-- Announces: "Cart total: $50.00" on price change -->

<!-- Announce only changed content -->
<div aria-live="polite" aria-atomic="false">
  <span>Item added:</span> <span>Blue Shirt</span>
</div>
<!-- Announces only: "Blue Shirt" on change -->

aria-relevant

What types of changes to announce:

<div aria-live="polite" aria-relevant="additions removals">
  <!-- Announces when items added or removed -->
</div>

Values: additions, removals, text, all

Live Region Patterns

Status messages:

<div role="status" aria-live="polite">
  <!-- Empty initially -->
</div>

<script>
  // After form submission
  statusDiv.textContent = 'Your message has been sent.';
</script>

Error alerts:

<div role="alert">
  <!-- Content immediately announced on render -->
  Unable to complete purchase. Please try again.
</div>

Shopping cart updates:

<div aria-live="polite" aria-atomic="true">
  <span class="visually-hidden">Cart:</span>
  <span id="cart-count">3</span> items
</div>

Common ARIA Patterns

Correct implementation patterns for frequent use cases.

Modals/Dialogs

<div role="dialog"
     aria-modal="true"
     aria-labelledby="dialog-title"
     aria-describedby="dialog-desc">
  <h2 id="dialog-title">Confirm Order</h2>
  <p id="dialog-desc">Review your order before confirming.</p>
  <!-- Dialog content -->
  <button>Cancel</button>
  <button>Confirm</button>
</div>

Required behaviors:

  • Focus moves to dialog on open
  • Focus trapped within dialog
  • Escape closes dialog
  • Focus returns to trigger on close

Tabs

<div class="tabs">
  <div role="tablist" aria-label="Product tabs">
    <button role="tab"
            id="tab-1"
            aria-selected="true"
            aria-controls="panel-1"
            tabindex="0">
      Description
    </button>
    <button role="tab"
            id="tab-2"
            aria-selected="false"
            aria-controls="panel-2"
            tabindex="-1">
      Specifications
    </button>
  </div>
  <div role="tabpanel"
       id="panel-1"
       aria-labelledby="tab-1"
       tabindex="0">
    Description content...
  </div>
  <div role="tabpanel"
       id="panel-2"
       aria-labelledby="tab-2"
       tabindex="0"
       hidden>
    Specifications content...
  </div>
</div>

Required behaviors:

  • Arrow keys navigate between tabs
  • Tab key moves to panel content
  • Only active tab is in tab order (tabindex="0")

Accordions

<div class="accordion">
  <h3>
    <button aria-expanded="true" aria-controls="section1">
      Section One
    </button>
  </h3>
  <div id="section1">
    Section one content...
  </div>

  <h3>
    <button aria-expanded="false" aria-controls="section2">
      Section Two
    </button>
  </h3>
  <div id="section2" hidden>
    Section two content...
  </div>
</div>

Navigation Menus

<nav aria-label="Main">
  <button aria-expanded="false" aria-haspopup="menu" aria-controls="menu-products">
    Products
  </button>
  <ul id="menu-products" role="menu" hidden>
    <li role="menuitem"><a href="/shirts">Shirts</a></li>
    <li role="menuitem"><a href="/pants">Pants</a></li>
    <li role="menuitem"><a href="/shoes">Shoes</a></li>
  </ul>
</nav>

Common ARIA Mistakes

Avoid these frequently encountered errors.

Using ARIA Instead of Native HTML

<!-- Wrong -->
<div role="button" tabindex="0">Submit</div>

<!-- Correct -->
<button>Submit</button>

Native HTML is always more robust.

Missing Required ARIA Attributes

Certain roles require specific attributes:

<!-- Wrong: checkbox without checked state -->
<div role="checkbox">Option</div>

<!-- Correct -->
<div role="checkbox" aria-checked="false">Option</div>

aria-label on Non-Interactive Elements

aria-label is ignored on many non-interactive elements:

<!-- Wrong: aria-label on span does nothing -->
<span aria-label="Important note">Note text</span>

<!-- Correct: Use visually hidden text or aria-describedby -->
<span><span class="visually-hidden">Important note:</span> Note text</span>

Hiding Content While Keeping It Focusable

<!-- Wrong: Hidden but still in tab order -->
<button aria-hidden="true">Hidden Button</button>

<!-- Correct: Hidden and not focusable -->
<button aria-hidden="true" tabindex="-1">Hidden Button</button>

Redundant ARIA

<!-- Redundant: button already announces as button -->
<button role="button">Submit</button>

<!-- Redundant: img alt provides name -->
<img src="logo.png" alt="Company Logo" aria-label="Company Logo">

<!-- Clean -->
<button>Submit</button>
<img src="logo.png" alt="Company Logo">

Conflicting ARIA

<!-- Conflicting: aria-label overrides visible text -->
<button aria-label="Close">
  Cancel
</button>
<!-- Screen reader announces "Close" but visual says "Cancel" -->

Testing ARIA Implementation

Verify ARIA works correctly through testing.

Browser DevTools

Inspect accessibility tree:

  1. Open DevTools → Accessibility panel
  2. Select elements to view computed accessibility properties
  3. Verify roles, states, and names match expectations

Screen Reader Testing

Test with actual screen readers:

  1. Navigate to ARIA-enhanced components
  2. Verify announcements match expectations
  3. Test state changes announce correctly
  4. Verify keyboard interaction works

See our Screen Reader Testing Guide.

Automated Validation

Tools detect common ARIA errors:

  • Missing required attributes
  • Invalid role combinations
  • Orphaned ARIA IDs
  • Hidden focusable elements

Taking Action

ARIA extends HTML accessibility for complex components, but misuse creates more problems than it solves. Always prefer native HTML elements. When ARIA is necessary, implement complete patterns including keyboard interaction, state management, and focus handling.

Start by auditing existing ARIA usage. Remove unnecessary ARIA from native elements. Ensure custom components using ARIA have complete keyboard support and appropriate state communication.

Schedule a TestParty demo and get a 14-day compliance implementation plan.


Related Resources

Contact Us

Automate the software work for accessibility compliance, end-to-end.

Empowering businesses with seamless digital accessibility solutions—simple, inclusive, effective.

Book a Demo