Detailed guide

Aria Labels Implementation Guide

View contents

ARIA Labels Implementation Guide: Complete Patterns for Accessible Naming

Introduction

ARIA labeling is more than just adding aria-label to elements. It requires understanding how browsers calculate accessible names, which techniques work for different contexts, and how to avoid common pitfalls that make accessibility worse instead of better. This guide provides comprehensive implementation patterns for all ARIA labeling scenarios.

The accessible name is what screen readers announce when users focus on an element. Getting this right is critical—a missing or unclear name makes elements unusable for screen reader users.

The Accessible Name Computation Algorithm

Priority Order

Browsers follow a specific algorithm (accname-1.2) to determine accessible names:

1. aria-labelledby (highest priority)
2. aria-label
3. Native labeling (<label>, <caption>, <legend>)
4. Element content (text nodes, alt text)
5. title attribute (lowest priority, avoid)

Understanding Precedence

<!-- aria-labelledby overrides everything -->
<button aria-labelledby="ext-label" aria-label="Ignored">
  Also ignored
</button>
<span id="ext-label">This is announced</span>
<!-- Announced: "This is announced, button" -->

<!-- aria-label overrides content -->
<button aria-label="This is announced">
  This is ignored
</button>
<!-- Announced: "This is announced, button" -->

<!-- Content is used when no ARIA -->
<button>This is announced</button>
<!-- Announced: "This is announced, button" -->

Name from Content

Some roles allow name from content (text inside element):

<!-- Roles that get name from content -->
<button>Submit Form</button>           <!-- ✅ Works -->
<a href="#">Learn More</a>             <!-- ✅ Works -->
<td>Cell value</td>                    <!-- ✅ Works -->
<h1>Page Title</h1>                    <!-- ✅ Works -->

<!-- Roles that DON'T get name from content -->
<div role="img">Description here</div> <!-- ❌ Needs aria-label -->
<nav>Navigation</nav>                   <!-- ❌ Text ignored for name -->
<section>Section text</section>         <!-- ❌ Text ignored for name -->

Implementing aria-label

Basic Usage

<!-- Icon buttons -->
<button aria-label="Close" class="close-btn">
  <svg aria-hidden="true">
    <use href="#icon-close"></use>
  </svg>
</button>

<!-- Search without visible label -->
<div class="search-container">
  <input type="search" aria-label="Search products">
  <button aria-label="Submit search">
    <svg aria-hidden="true"><!-- search icon --></svg>
  </button>
</div>

<!-- Navigation landmarks -->
<nav aria-label="Main">...</nav>
<nav aria-label="Footer">...</nav>
<nav aria-label="Breadcrumb">...</nav>

Dynamic aria-label

// React example
function ToggleButton({ isActive, label }) {
  return (
    <button
      aria-label={`${isActive ? 'Disable' : 'Enable'} ${label}`}
      aria-pressed={isActive}
      onClick={toggle}
    >
      <Icon name={isActive ? 'check' : 'circle'} />
    </button>
  );
}

// Vue 3 example
<template>
  <button
    :aria-label="`${isActive ? 'Desactivar' : 'Activar'} ${label}`"
    :aria-pressed="isActive"
    @click="toggle"
  >
    <Icon :name="isActive ? 'check' : 'circle'" />
  </button>
</template>

When NOT to Use aria-label

<!-- DON'T: On elements that already have visible text -->
<button aria-label="Submit">Submit</button>
<!-- Redundant - just use content -->

<!-- DON'T: Different from visible text -->
<button aria-label="Send message">Submit</button>
<!-- Confuses voice control users -->

<!-- DON'T: On non-interactive elements -->
<p aria-label="Important paragraph">Text here</p>
<!-- Screen readers may ignore -->

<!-- DON'T: To add long descriptions -->
<button aria-label="Click this button to submit the form and you will receive a confirmation email within 24 hours">
  Submit
</button>
<!-- Use aria-describedby for long descriptions -->

Implementing aria-labelledby

Referencing Existing Text

<!-- Single reference -->
<h2 id="cart-heading">Shopping Cart (3 items)</h2>
<section aria-labelledby="cart-heading">
  <!-- Cart contents -->
</section>

<!-- Multiple references (space-separated) -->
<span id="fname-label">First Name</span>
<span id="fname-required">(required)</span>
<span id="fname-format">Letters only</span>
<input
  type="text"
  aria-labelledby="fname-label fname-required"
  aria-describedby="fname-format"
>
<!-- Name: "First Name (required)" -->
<!-- Description: "Letters only" -->

Self-Reference Pattern

<!-- Include element's own content in label -->
<button
  id="buy-btn"
  aria-labelledby="buy-btn product-name"
>
  Buy
</button>
<span id="product-name">Wireless Mouse</span>
<!-- Announced: "Buy Wireless Mouse, button" -->

Dialog Labeling

<div
  role="dialog"
  aria-labelledby="dialog-title"
  aria-describedby="dialog-desc"
>
  <h2 id="dialog-title">Confirm Deletion</h2>
  <p id="dialog-desc">
    Are you sure you want to delete this item?
    This action cannot be undone.
  </p>
  <button>Cancel</button>
  <button>Delete</button>
</div>

Table and Grid Labeling

<!-- Table with caption -->
<table aria-labelledby="table-caption">
  <caption id="table-caption">Quarterly Sales Report 2024</caption>
  <thead>...</thead>
  <tbody>...</tbody>
</table>

<!-- Grid with external heading -->
<h2 id="grid-title">Product Comparison</h2>
<div role="grid" aria-labelledby="grid-title">
  <div role="row">
    <div role="columnheader">Feature</div>
    <div role="columnheader">Basic</div>
    <div role="columnheader">Pro</div>
  </div>
  <!-- rows -->
</div>

Implementing aria-describedby

Form Field Help Text

<div class="form-group">
  <label for="username">Username</label>
  <input
    type="text"
    id="username"
    aria-describedby="username-help username-error"
  >
  <p id="username-help" class="help-text">
    3-20 characters, letters and numbers only
  </p>
  <p id="username-error" class="error" hidden>
    Username already taken
  </p>
</div>
// Show error when validation fails
function showError(input, errorElement) {
  errorElement.hidden = false;
  input.setAttribute('aria-invalid', 'true');
  // aria-describedby already includes error ID
}

Complex Widget Descriptions

<!-- Slider with description -->
<div class="slider-container">
  <label id="volume-label">Volume</label>
  <div
    role="slider"
    aria-labelledby="volume-label"
    aria-describedby="volume-desc"
    aria-valuemin="0"
    aria-valuemax="100"
    aria-valuenow="50"
    tabindex="0"
  >
    <!-- slider track and thumb -->
  </div>
  <p id="volume-desc">
    Use arrow keys to adjust. Current value shown as percentage.
  </p>
</div>

Component-Specific Patterns

Accessible Icon Buttons

<!-- Simple icon button -->
<button aria-label="Settings" class="icon-btn">
  <svg aria-hidden="true" focusable="false">
    <use href="#icon-settings"></use>
  </svg>
</button>

<!-- Icon button with badge -->
<button aria-label="Notifications, 5 unread" class="icon-btn">
  <svg aria-hidden="true" focusable="false">
    <use href="#icon-bell"></use>
  </svg>
  <span class="badge" aria-hidden="true">5</span>
</button>

<!-- Toggle icon button -->
<button
  aria-label="Bookmark"
  aria-pressed="false"
  class="icon-btn"
>
  <svg aria-hidden="true" focusable="false">
    <use href="#icon-bookmark"></use>
  </svg>
</button>

Accessible Cards

<article class="card" aria-labelledby="card-1-title">
  <img src="product.jpg" alt="">
  <h3 id="card-1-title">Wireless Headphones</h3>
  <p>High-quality audio with 30-hour battery life.</p>
  <p class="price">$149.99</p>
  <a
    href="/products/wireless-headphones"
    aria-label="View Wireless Headphones details"
  >
    View Details
  </a>
</article>

Accessible Tabs

<div class="tabs-container">
  <div role="tablist" aria-label="Account Settings">
    <button
      role="tab"
      id="tab-profile"
      aria-selected="true"
      aria-controls="panel-profile"
    >
      Profile
    </button>
    <button
      role="tab"
      id="tab-security"
      aria-selected="false"
      aria-controls="panel-security"
      tabindex="-1"
    >
      Security
    </button>
    <button
      role="tab"
      id="tab-notifications"
      aria-selected="false"
      aria-controls="panel-notifications"
      tabindex="-1"
    >
      Notifications
    </button>
  </div>

  <div
    role="tabpanel"
    id="panel-profile"
    aria-labelledby="tab-profile"
  >
    <!-- Profile content -->
  </div>
</div>

Accessible Accordion

<div class="accordion">
  <h3>
    <button
      aria-expanded="true"
      aria-controls="section1-content"
      id="section1-header"
    >
      Shipping Information
    </button>
  </h3>
  <div
    id="section1-content"
    role="region"
    aria-labelledby="section1-header"
  >
    <p>We ship worldwide with free shipping on orders over $50.</p>
  </div>

  <h3>
    <button
      aria-expanded="false"
      aria-controls="section2-content"
      id="section2-header"
    >
      Return Policy
    </button>
  </h3>
  <div
    id="section2-content"
    role="region"
    aria-labelledby="section2-header"
    hidden
  >
    <p>Returns accepted within 30 days of purchase.</p>
  </div>
</div>

Framework Integration

React Patterns

// Accessible button component
interface IconButtonProps {
  icon: string;
  label: string;
  onClick: () => void;
  badge?: number;
}

function IconButton({ icon, label, onClick, badge }: IconButtonProps) {
  const computedLabel = badge
    ? `${label}, ${badge} ${badge === 1 ? 'item' : 'items'}`
    : label;

  return (
    <button
      aria-label={computedLabel}
      onClick={onClick}
      className="icon-btn"
    >
      <Icon name={icon} aria-hidden="true" />
      {badge && (
        <span className="badge" aria-hidden="true">{badge}</span>
      )}
    </button>
  );
}

// Usage
<IconButton
  icon="cart"
  label="Shopping cart"
  badge={3}
  onClick={openCart}
/>

Vue 3 Patterns

<!-- AccessibleDialog.vue -->
<script setup lang="ts">
import { ref, computed } from 'vue'

const props = defineProps<{
  title: string
  description?: string
}>()

const titleId = computed(() => `dialog-title-${crypto.randomUUID()}`)
const descId = computed(() => `dialog-desc-${crypto.randomUUID()}`)
</script>

<template>
  <div
    role="dialog"
    aria-modal="true"
    :aria-labelledby="titleId"
    :aria-describedby="description ? descId : undefined"
  >
    <h2 :id="titleId">{{ title }}</h2>
    <p v-if="description" :id="descId">{{ description }}</p>
    <slot />
  </div>
</template>

Testing ARIA Labels

Automated Testing

// Jest + Testing Library
import { render, screen } from '@testing-library/react';

test('icon button has accessible name', () => {
  render(<IconButton icon="close" label="Close dialog" onClick={jest.fn()} />);

  const button = screen.getByRole('button', { name: 'Close dialog' });
  expect(button).toBeInTheDocument();
});

test('dialog is properly labeled', () => {
  render(<Dialog title="Confirm Action" description="Are you sure?" />);

  const dialog = screen.getByRole('dialog', { name: 'Confirm Action' });
  expect(dialog).toHaveAccessibleDescription('Are you sure?');
});

Playwright Accessibility Testing

// a11y.spec.js
const { test, expect } = require('@playwright/test');
const AxeBuilder = require('@axe-core/playwright').default;

test('all buttons have accessible names', async ({ page }) => {
  await page.goto('/');

  const buttons = await page.locator('button').all();

  for (const button of buttons) {
    const name = await button.getAttribute('aria-label') ||
                 await button.textContent();
    expect(name?.trim()).toBeTruthy();
  }
});

test('no axe violations for labeling', async ({ page }) => {
  await page.goto('/');

  const results = await new AxeBuilder({ page })
    .withRules(['button-name', 'link-name', 'image-alt'])
    .analyze();

  expect(results.violations).toHaveLength(0);
});

Common Pitfalls and Solutions

Pitfall 1: Empty References

<!-- BAD: Reference doesn't exist -->
<button aria-labelledby="nonexistent">Click</button>
<!-- Name is empty! -->

<!-- GOOD: Ensure reference exists -->
<span id="btn-label" class="sr-only">Submit form</span>
<button aria-labelledby="btn-label">
  <svg>...</svg>
</button>

Pitfall 2: Overusing ARIA

<!-- BAD: ARIA where not needed -->
<button aria-label="Submit" role="button">Submit</button>

<!-- GOOD: Native elements are already accessible -->
<button>Submit</button>

Pitfall 3: Inconsistent Labeling

<!-- BAD: Same action, different labels -->
<button aria-label="Close">X</button>
<button aria-label="Dismiss">X</button>
<button aria-label="Exit">X</button>

<!-- GOOD: Consistent labeling -->
<button aria-label="Close">X</button>
<button aria-label="Close">X</button>
<button aria-label="Close">X</button>

Summary Checklist

Implementation Checklist

  • [ ] All interactive elements have accessible names
  • [ ] Icon-only buttons use aria-label
  • [ ] Complex widgets use aria-labelledby for visible labels
  • [ ] Help text uses aria-describedby
  • [ ] Multiple nav landmarks are distinguished
  • [ ] Dialogs have title and description
  • [ ] No redundant labeling
  • [ ] Labels match visible text when present
  • [ ] Dynamic labels update appropriately
  • [ ] Tested with screen readers

References

  1. W3C - WCAG 2.2 SC 4.1.2 Name, Role, Value
  2. W3C - Accessible Name and Description Computation
  3. W3C - ARIA Authoring Practices
  4. WebAIM - ARIA Techniques
  5. Deque - Accessible Name Calculation

Related articles

In the same category

Introduction

Ai Crawlability Explained

As AI assistants like ChatGPT, Claude, Gemini, and Perplexity become primary information sources, a new question emerges: Should you allow AI bots to access...

Detailed guide

Ai Crawler Management Guide

Managing AI crawler access requires understanding the diverse landscape of AI bots, their purposes, and the technical mechanisms to control them

Introduction

Alt Text Explained

Alt text (alternative text) provides a text description of images for users who cannot see them

Detailed guide

Alt Text Implementation Guide

This comprehensive guide covers the technical implementation of alt text across all image types and contexts

Introduction

Alt Text Seo Explained

Every image on your website is either helping or hurting your SEO

Detailed guide

Alt Text Seo Optimization Guide

Alt text optimization sits at the intersection of SEO, accessibility, and user experience