UXR SEO Analyzer documentation

Introduction

ARIA Labels

View contents

Introduction

ARIA labels provide accessible names for elements that screen readers announce to users. When native HTML doesn’t provide a clear name—like icon-only buttons or custom widgets—ARIA attributes fill the gap. WCAG 4.1.2 requires that all user interface components have programmatically determinable names, making ARIA labeling essential for accessibility.

Without proper accessible names, screen reader users hear meaningless announcements like “button” or “link” without understanding what the element does. ARIA labels ensure every interactive element communicates its purpose clearly.

Understanding Accessible Names

What Is an Accessible Name?

An accessible name is the text that assistive technologies announce when a user focuses on an element. It can come from various sources:

Source Priority Example
aria-labelledby Highest References another element’s text
aria-label High Direct label text
<label> element Medium Form field labels
Element content Lower Text inside buttons/links
title attribute Lowest Last resort option

The Name Calculation

Browsers use a specific algorithm to determine accessible names:

<!-- 1. aria-labelledby wins (highest priority) -->
<button aria-labelledby="btn-label">
  <svg>...</svg>
</button>
<span id="btn-label">Search products</span>
<!-- Announced: "Search products, button" -->

<!-- 2. aria-label is next -->
<button aria-label="Search products">
  <svg>...</svg>
</button>
<!-- Announced: "Search products, button" -->

<!-- 3. Content is lower priority -->
<button>
  <svg aria-hidden="true">...</svg>
  Search
</button>
<!-- Announced: "Search, button" -->

ARIA Labeling Attributes

aria-label

Provides a string directly as the accessible name:

<!-- Icon-only button -->
<button aria-label="Close dialog">
  <svg aria-hidden="true"><!-- X icon --></svg>
</button>

<!-- Search input -->
<input type="search" aria-label="Search products">

Best for:

  • Icon-only buttons
  • Inputs without visible labels
  • Elements where visual context is clear

Limitations:

  • Not translatable by browser translation tools
  • Not visible to sighted users

aria-labelledby

References another element’s text as the label:

<!-- Using existing heading -->
<h2 id="section-title">Shopping Cart</h2>
<nav aria-labelledby="section-title">
  <!-- Navigation within shopping cart -->
</nav>

<!-- Multiple elements -->
<span id="label">Email</span>
<span id="hint">(required)</span>
<input type="email" aria-labelledby="label hint">
<!-- Announced: "Email (required)" -->

Best for:

  • When label text already exists visually
  • Combining multiple text sources
  • Sections labeled by headings

aria-describedby

Provides supplementary description (not the primary name):

<label for="password">Password</label>
<input type="password" id="password" aria-describedby="pwd-help">
<p id="pwd-help">Must be at least 8 characters with one number</p>
<!-- Announced: "Password, edit text, Must be at least 8 characters..." -->

Difference from labelledby:

  • aria-labelledby = primary name (announced first)
  • aria-describedby = additional description (announced after)

Common ARIA Label Use Cases

Icon Buttons

<!-- Social media icons -->
<a href="https://twitter.com/company" aria-label="Follow us on Twitter">
  <svg aria-hidden="true"><!-- Twitter icon --></svg>
</a>

<button aria-label="Add to favorites">
  <svg aria-hidden="true"><!-- Heart icon --></svg>
</button>
<!-- Multiple nav elements need distinction -->
<nav aria-label="Main navigation">
  <!-- Primary navigation -->
</nav>

<nav aria-label="Footer navigation">
  <!-- Footer links -->
</nav>

<nav aria-label="Breadcrumb">
  <!-- Breadcrumb trail -->
</nav>

Form Regions

<section aria-labelledby="shipping-title">
  <h2 id="shipping-title">Shipping Information</h2>
  <!-- Shipping form fields -->
</section>

<section aria-labelledby="billing-title">
  <h2 id="billing-title">Billing Information</h2>
  <!-- Billing form fields -->
</section>

Custom Widgets

<!-- Star rating -->
<div role="slider"
     aria-label="Rating"
     aria-valuemin="1"
     aria-valuemax="5"
     aria-valuenow="4">
  <!-- Star icons -->
</div>

<!-- Toggle switch -->
<button role="switch"
        aria-checked="false"
        aria-label="Dark mode">
  <!-- Toggle visual -->
</button>

Common Labeling Mistakes

1. Missing Labels

<!-- BAD: No accessible name -->
<button>
  <svg><!-- icon --></svg>
</button>
<!-- Announced: "button" - meaningless! -->

<!-- GOOD: Has accessible name -->
<button aria-label="Delete item">
  <svg aria-hidden="true"><!-- icon --></svg>
</button>

2. Redundant Labels

<!-- BAD: Redundant information -->
<button aria-label="Submit button">Submit</button>
<!-- Announced: "Submit button, button" - "button" is said twice -->

<!-- GOOD: Let content be the name -->
<button>Submit</button>
<!-- Announced: "Submit, button" -->

3. Labels Not Matching Visible Text

<!-- BAD: Different from visible text -->
<button aria-label="Send message">Submit Form</button>
<!-- Voice control users say "click Submit Form" but it won't work -->

<!-- GOOD: Match or include visible text -->
<button aria-label="Submit Form - sends your message">Submit Form</button>

4. Using aria-label on Non-Interactive Elements

<!-- BAD: aria-label on div (screen readers may ignore) -->
<div aria-label="Important section">Content here</div>

<!-- GOOD: Use on landmarks or interactive elements -->
<section aria-label="Important section">Content here</section>

Testing ARIA Labels

Quick Manual Test

  1. Open browser DevTools
  2. Inspect an element
  3. Check the Accessibility panel
  4. Look for “Name” property
  5. Verify it makes sense

Using Screen Readers

Screen Reader Command
NVDA Tab to element, listen
VoiceOver VO+Right arrow
JAWS Tab to element
Narrator Tab to element

Automated Tools

  • axe DevTools - Reports missing accessible names
  • WAVE - Highlights unlabeled elements
  • Lighthouse - Checks for accessible names
  • Browser DevTools - Shows computed accessible name

Best Practices Summary

Do Don’t
Use aria-label for icon-only elements Put aria-label on non-interactive divs
Use aria-labelledby when text exists Duplicate visible text in aria-label
Make labels concise but clear Write overly long labels
Start with action verb for buttons Include element type in label
Test with actual screen readers Assume visual clarity = accessibility

When to Use Each Attribute

Situation Use
Icon-only button aria-label
Existing visual label nearby aria-labelledby
Form field without visible label aria-label
Additional help text aria-describedby
Multiple nav landmarks aria-label on each
Section with heading aria-labelledby to heading

Implementation

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 Computation
  3. WebAIM - ARIA
  4. MDN - Using aria-label
  5. W3C - ARIA Authoring Practices
  6. Deque - Accessible Name Calculation

Related articles

In the same category

Introduction

AI Crawlability

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 for Screen Readers

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

Introduction

Alt Text for SEO: How to Write It by Image Type

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

Detailed guide

Alt Text Optimization: Keywords, Automation and Auditing

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

Introduction

HTTP Caching Headers

HTTP caching allows browsers to store copies of resources locally, eliminating the need to download them again on subsequent visits

Last updated: