Skip to main content
Back to Blog
Digital Accessibility

ARIA Labels for Web Accessibility: Complete 2025 Implementation Guide

Master ARIA labels, roles, and states for WCAG 2.2 compliance. Comprehensive guide covering aria-label, aria-labelledby, aria-describedby, live regions, and modern best practices for accessible web applications in 2025.

AllAccessible Team
20 min read
digital accessibilityARIA labelsWCAG 2.2aria-labelaria-labelledbyscreen readersaccessible web applicationsARIA best practicessemantic HTML
ARIA Labels for Web Accessibility: Complete 2025 Implementation Guide

ARIA Labels for Web Accessibility: Complete 2025 Implementation Guide

15% of the world's population lives with some form of disability. For the 12 million Americans with visual impairments who rely on screen readers, proper ARIA implementation is the difference between accessing your website and being completely excluded.

With WCAG 2.2 now the legal standard (referenced in 4,605 ADA lawsuits in 2024) and the ARIA 1.3 specification providing enhanced capabilities, understanding ARIA labels is essential for creating accessible web applications in 2025.

Table of Contents

  1. What is ARIA and Why Does it Matter?
  2. ARIA and WCAG 2.2 Compliance
  3. The First Rule of ARIA
  4. ARIA Labeling Techniques
  5. ARIA Roles for Structure and Navigation
  6. ARIA States and Properties
  7. ARIA Live Regions
  8. ARIA in Modern Frameworks
  9. Common ARIA Mistakes to Avoid
  10. Testing ARIA Implementation
  11. ARIA Best Practices 2025

What is ARIA and Why Does it Matter? {#what-is-aria}

ARIA (Accessible Rich Internet Applications) is a set of attributes that enhance the accessibility of dynamic web content and advanced user interface controls for people using assistive technologies.

The Problem ARIA Solves

Native HTML provides limited semantics:

<!-- Screen readers can't tell what this div is for -->
<div class="modal" style="display: none;">
    <div class="modal-content">
        <span class="close">×</span>
        <h2>Confirmation</h2>
        <p>Are you sure you want to delete this item?</p>
        <button>Yes</button>
        <button>No</button>
    </div>
</div>

ARIA adds meaning:

<!-- Now screen readers understand this is a modal dialog -->
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="dialog-title"
     aria-describedby="dialog-desc" style="display: none;">
    <div class="modal-content">
        <button class="close" aria-label="Close dialog">×</button>
        <h2 id="dialog-title">Confirmation</h2>
        <p id="dialog-desc">Are you sure you want to delete this item?</p>
        <button>Yes</button>
        <button>No</button>
    </div>
</div>

What ARIA Can Do

1. Add Labels and Descriptions

  • aria-label: Provides accessible name for elements
  • aria-labelledby: References existing elements for label
  • aria-describedby: Adds additional description

2. Define Roles

  • role="navigation": Navigation landmark
  • role="dialog": Modal dialog
  • role="tab": Tab in tablist

3. Communicate State

  • aria-expanded="true": Accordion is open
  • aria-selected="true": Tab is active
  • aria-disabled="true": Button is disabled

4. Announce Dynamic Changes

  • aria-live="polite": Announce updates when convenient
  • aria-live="assertive": Announce updates immediately
  • role="alert": Important message

The Impact of Proper ARIA

Without ARIA:

  • Screen reader users hear "button, button, button" with no context
  • Complex widgets appear as disconnected elements
  • Dynamic updates go unnoticed
  • Navigation structure unclear

With ARIA:

  • Screen readers announce "Close dialog button"
  • Tabs, accordions, and menus work as expected
  • Users hear announcements when content updates
  • Keyboard users can navigate efficiently

ARIA and WCAG 2.2 Compliance {#wcag-compliance}

WCAG 2.2 (released October 2023) is now the de facto legal standard. Here's how ARIA relates to key success criteria:

WCAG Success Criteria Requiring ARIA

1.3.1 Info and Relationships (Level A)

  • ARIA roles must accurately describe relationships
  • Example: role="list" with role="listitem" for custom lists

2.4.6 Headings and Labels (Level AA)

  • Labels must describe purpose
  • Use aria-label or aria-labelledby for form controls

4.1.2 Name, Role, Value (Level A)

  • All UI components must have accessible names
  • ARIA labels provide names for custom widgets

4.1.3 Status Messages (Level AA)

  • Dynamic status messages must be announced
  • Use role="status" or aria-live regions

Legal Implications

4,605 ADA website lawsuits filed in 2024:

  • 92% cite WCAG 2.1/2.2 Level AA as standard
  • Common violations: Missing ARIA labels on form fields
  • Average settlement: $25,000 - $75,000

Example lawsuit citation:

"The website failed to provide accessible names for form controls, violating WCAG 4.1.2. Screen reader users could not identify the purpose of input fields due to missing aria-label attributes."


The First Rule of ARIA {#first-rule}

The First Rule of ARIA: Don't use ARIA.

This isn't a joke—it's official guidance from the W3C. The full rule states:

"If you can use a native HTML element or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so."

Why Semantic HTML Comes First

Wrong approach (unnecessary ARIA):

<div role="button" tabindex="0" onclick="submit()" onkeypress="submit()">
    Submit Form
</div>

Right approach (native HTML):

<button type="submit">Submit Form</button>

The native <button> provides:

  • Automatic keyboard accessibility (Space and Enter keys)
  • Focus management
  • Screen reader announcement as "button"
  • Form submission behavior
  • No ARIA needed

When ARIA is Necessary

Use ARIA when:

  1. No native HTML element exists (custom tabs, accordions)
  2. Native element lacks needed semantics (generic <div> containers)
  3. Enhancing existing elements with additional context
  4. Creating custom widgets and components

Examples where ARIA is appropriate:

  • Custom tab panels (role="tabpanel")
  • Accordion widgets (aria-expanded)
  • Modal dialogs (role="dialog", aria-modal)
  • Live regions for dynamic content (aria-live)
  • Application-specific landmarks (role="search", role="navigation")

ARIA Labeling Techniques {#labeling-techniques}

There are three main ways to provide accessible labels using ARIA:

1. aria-label (Direct Label)

Best for: Elements with no visible text label

<!-- Icon-only button -->
<button aria-label="Close modal" class="close-btn">
    <svg><!-- X icon --></svg>
</button>

<!-- Search input with no visible label -->
<input type="search" aria-label="Search products" placeholder="Search...">

<!-- Social media link with only icon -->
<a href="https://twitter.com/example" aria-label="Follow us on Twitter">
    <svg><!-- Twitter icon --></svg>
</a>

When to use:

  • Icon-only buttons
  • Search inputs without visible labels
  • Links with only images or icons
  • Elements where visible label doesn't match accessible name needed

Pros:

  • Simple and direct
  • Works when no existing label text available
  • Overrides any other labeling method

Cons:

  • Doesn't get translated by browser translation tools
  • Not visible to sighted users
  • Can create mismatch between visible and accessible names

2. aria-labelledby (Reference Existing Element)

Best for: Associating element with existing visible text

<!-- Modal dialog labeled by its heading -->
<div role="dialog" aria-labelledby="dialog-title">
    <h2 id="dialog-title">Confirm Delete</h2>
    <p>Are you sure you want to delete this item?</p>
    <button>Yes</button>
    <button>No</button>
</div>

<!-- Form section labeled by heading -->
<section aria-labelledby="shipping-heading">
    <h3 id="shipping-heading">Shipping Information</h3>
    <label for="address">Address</label>
    <input type="text" id="address">
</section>

<!-- Custom dropdown labeled by visible text -->
<label id="color-label">Select Color</label>
<div role="combobox" aria-labelledby="color-label" aria-expanded="false">
    <span>Choose...</span>
</div>

Advanced: Multiple references

<!-- Label combines multiple elements -->
<div id="row-label">Product:</div>
<div id="product-name">Organic Cotton T-Shirt</div>

<button aria-labelledby="row-label product-name delete-action">
    <span id="delete-action" hidden>Delete</span>
    <svg><!-- Trash icon --></svg>
</button>

<!-- Screen reader announces: "Delete Product: Organic Cotton T-Shirt" -->

When to use:

  • Visible label text exists on page
  • Combining multiple text sources
  • Dialogs, regions, or sections with headings
  • Custom form controls

Pros:

  • Uses visible text (consistency)
  • Can combine multiple label sources
  • Gets translated by browser tools
  • Visible to all users

Cons:

  • Requires ID references
  • Referenced elements must exist on page

3. aria-describedby (Additional Description)

Best for: Supplementary information, not primary label

<!-- Form input with help text -->
<label for="password">Password</label>
<input type="password"
       id="password"
       aria-describedby="password-help password-error">
<span id="password-help">Must be at least 12 characters</span>
<span id="password-error" role="alert" hidden>Password too short</span>

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

<!-- Link with expanded description -->
<a href="/report.pdf" aria-describedby="file-desc">
    Annual Report
    <span id="file-desc">(PDF, 2.5 MB)</span>
</a>

When to use:

  • Providing help text for form fields
  • Error messages for validation
  • File type and size information
  • Warnings or additional context

Pros:

  • Doesn't replace primary label
  • Can reference multiple descriptions
  • Announced after primary label

Cons:

  • Not always announced by all screen readers
  • Should not be primary labeling method

ARIA Roles for Structure and Navigation {#aria-roles}

ARIA roles define what an element is or does. There are three categories:

1. Landmark Roles (Page Structure)

Landmarks help screen reader users navigate:

<!-- Main navigation -->
<nav role="navigation" aria-label="Main navigation">
    <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/products">Products</a></li>
    </ul>
</nav>

<!-- Search form -->
<form role="search" aria-label="Site search">
    <label for="search-input">Search</label>
    <input type="search" id="search-input">
    <button type="submit">Search</button>
</form>

<!-- Main content area -->
<main role="main" aria-label="Main content">
    <!-- Page content -->
</main>

<!-- Complementary sidebar -->
<aside role="complementary" aria-label="Related articles">
    <!-- Sidebar content -->
</aside>

<!-- Page footer -->
<footer role="contentinfo" aria-label="Site footer">
    <!-- Footer content -->
</footer>

<!-- Banner/header -->
<header role="banner" aria-label="Site header">
    <img src="logo.png" alt="Company Name">
</header>

Key landmark roles:

  • role="banner" - Site header (one per page)
  • role="navigation" - Navigation menus (multiple allowed)
  • role="main" - Primary content (one per page)
  • role="search" - Search functionality
  • role="complementary" - Supporting content (sidebars)
  • role="contentinfo" - Site footer (one per page)

Note: HTML5 semantic elements (<nav>, <main>, <aside>, etc.) have implicit landmark roles, but adding explicit role attributes improves support in older assistive technologies.


2. Widget Roles (Interactive Components)

Custom widgets need explicit roles:

<!-- Tab interface -->
<div class="tabs">
    <div role="tablist" aria-label="Product information">
        <button role="tab" aria-selected="true" aria-controls="desc-panel" id="desc-tab">
            Description
        </button>
        <button role="tab" aria-selected="false" aria-controls="specs-panel" id="specs-tab">
            Specifications
        </button>
    </div>

    <div role="tabpanel" id="desc-panel" aria-labelledby="desc-tab">
        Product description content...
    </div>

    <div role="tabpanel" id="specs-panel" aria-labelledby="specs-tab" hidden>
        Product specifications...
    </div>
</div>

<!-- Accordion -->
<div class="accordion">
    <h3>
        <button aria-expanded="false" aria-controls="panel1">
            Section 1
        </button>
    </h3>
    <div id="panel1" role="region" aria-labelledby="button1" hidden>
        Content for section 1...
    </div>
</div>

<!-- Custom dropdown -->
<div class="dropdown">
    <button aria-haspopup="listbox"
            aria-expanded="false"
            aria-controls="options-list">
        Select option
    </button>

    <ul role="listbox" id="options-list" hidden>
        <li role="option">Option 1</li>
        <li role="option">Option 2</li>
    </ul>
</div>

<!-- Dialog/Modal -->
<div role="dialog"
     aria-modal="true"
     aria-labelledby="dialog-title"
     aria-describedby="dialog-desc">
    <h2 id="dialog-title">Confirmation Required</h2>
    <p id="dialog-desc">Are you sure you want to proceed?</p>
    <button>Confirm</button>
    <button>Cancel</button>
</div>

Common widget roles:

  • role="button" - Clickable button
  • role="tab", role="tablist", role="tabpanel" - Tab interface
  • role="dialog" - Modal dialog
  • role="menu", role="menuitem" - Application menu
  • role="combobox", role="listbox", role="option" - Dropdown
  • role="slider" - Range slider
  • role="progressbar" - Progress indicator

3. Document Structure Roles

For complex content structure:

<!-- Article with sections -->
<article role="article">
    <h1>Blog Post Title</h1>

    <section role="region" aria-labelledby="intro-heading">
        <h2 id="intro-heading">Introduction</h2>
        <p>Introduction content...</p>
    </section>

    <section role="region" aria-labelledby="main-heading">
        <h2 id="main-heading">Main Points</h2>
        <p>Main content...</p>
    </section>
</article>

<!-- List with custom styling -->
<div role="list" aria-label="Team members">
    <div role="listitem">
        <img src="person1.jpg" alt="Sarah Johnson">
        <h3>Sarah Johnson</h3>
        <p>CEO</p>
    </div>
    <div role="listitem">
        <img src="person2.jpg" alt="Mike Chen">
        <h3>Mike Chen</h3>
        <p>CTO</p>
    </div>
</div>

<!-- Data table -->
<div role="table" aria-label="Product comparison">
    <div role="rowgroup">
        <div role="row">
            <span role="columnheader">Feature</span>
            <span role="columnheader">Basic</span>
            <span role="columnheader">Pro</span>
        </div>
    </div>
    <div role="rowgroup">
        <div role="row">
            <span role="cell">Storage</span>
            <span role="cell">10 GB</span>
            <span role="cell">100 GB</span>
        </div>
    </div>
</div>

ARIA States and Properties {#states-properties}

ARIA states change based on user interaction. ARIA properties define relationships and characteristics.

Common ARIA States

<!-- aria-expanded (collapsed/expanded state) -->
<button aria-expanded="false" aria-controls="menu">
    Menu
    <span aria-hidden="true">▼</span>
</button>
<ul id="menu" hidden>
    <li><a href="/home">Home</a></li>
    <li><a href="/about">About</a></li>
</ul>

<!-- aria-selected (selected state in tabs/lists) -->
<div role="tablist">
    <button role="tab" aria-selected="true">Tab 1</button>
    <button role="tab" aria-selected="false">Tab 2</button>
</div>

<!-- aria-checked (checkbox state) -->
<div role="checkbox" aria-checked="true" tabindex="0">
    <span aria-hidden="true">✓</span> Remember me
</div>

<!-- aria-pressed (toggle button state) -->
<button aria-pressed="false">
    Bold
</button>

<!-- aria-disabled (disabled state) -->
<button aria-disabled="true">
    Submit (Please fill all required fields)
</button>

<!-- aria-current (current item in navigation) -->
<nav>
    <a href="/" aria-current="page">Home</a>
    <a href="/about">About</a>
    <a href="/contact">Contact</a>
</nav>

<!-- aria-hidden (hide from assistive technology) -->
<button aria-label="Close">
    <span aria-hidden="true">×</span>
</button>

Common ARIA Properties

<!-- aria-haspopup (indicates popup menu/dialog) -->
<button aria-haspopup="menu" aria-expanded="false">
    Options
</button>

<!-- aria-controls (relationship to controlled element) -->
<button aria-controls="accordion-content" aria-expanded="false">
    Show More
</button>
<div id="accordion-content" hidden>
    Content here...
</div>

<!-- aria-owns (relationship when DOM hierarchy doesn't match) -->
<div role="listbox" aria-owns="option1 option2 option3">
    <div role="option" id="option1">Option 1</div>
</div>
<!-- Options rendered elsewhere in DOM -->
<div id="option2" role="option">Option 2</div>
<div id="option3" role="option">Option 3</div>

<!-- aria-required (required form field) -->
<label for="email">
    Email <span aria-hidden="true">*</span>
</label>
<input type="email" id="email" aria-required="true" required>

<!-- aria-invalid (field validation state) -->
<label for="password">Password</label>
<input type="password"
       id="password"
       aria-invalid="true"
       aria-describedby="password-error">
<span id="password-error" role="alert">
    Password must be at least 12 characters
</span>

ARIA Live Regions {#live-regions}

ARIA live regions announce dynamic content changes to screen reader users.

aria-live Politeness Levels

<!-- aria-live="polite" - Announce when convenient -->
<div aria-live="polite" aria-atomic="true" class="status-message">
    <span id="save-status"></span>
</div>

<script>
// Updates announced after current speech finishes
document.getElementById('save-status').textContent = 'Draft saved at 3:45 PM';
</script>

<!-- aria-live="assertive" - Announce immediately -->
<div aria-live="assertive" aria-atomic="true" class="error-message">
    <span id="error-announce"></span>
</div>

<script>
// Interrupts current speech to announce error
document.getElementById('error-announce').textContent = 'Error: Connection lost';
</script>

<!-- aria-live="off" - Don't announce (default) -->
<div aria-live="off">
    This content won't be announced when updated
</div>

role="status" (Polite Live Region)

For non-critical status updates:

<!-- Search results count -->
<div role="status" aria-live="polite" aria-atomic="true">
    <span id="results-count">Showing 24 results</span>
</div>

<!-- Form validation feedback -->
<div role="status" aria-live="polite">
    <span id="validation-status"></span>
</div>

<script>
document.getElementById('validation-status').textContent = 'All fields completed';
</script>

<!-- Shopping cart updates -->
<div role="status" aria-live="polite" aria-atomic="true">
    <span id="cart-status">Your cart contains 3 items totaling $147.99</span>
</div>

role="alert" (Assertive Live Region)

For important, time-sensitive information:

<!-- Error messages -->
<div role="alert" aria-live="assertive" class="error-alert">
    <span id="error-message">Invalid email address format</span>
</div>

<!-- Session timeout warning -->
<div role="alert" class="timeout-warning" hidden>
    <p>Your session will expire in 2 minutes due to inactivity.</p>
    <button>Continue Session</button>
</div>

<!-- Payment processing status -->
<div role="alert" aria-live="assertive">
    <span id="payment-status"></span>
</div>

<script>
document.getElementById('payment-status').textContent =
    'Payment failed. Please check your card details.';
</script>

aria-atomic and aria-relevant

<!-- aria-atomic="true" - Announce entire region -->
<div aria-live="polite" aria-atomic="true">
    <span>Items in cart:</span>
    <span id="cart-count">3</span>
</div>
<!-- Announces: "Items in cart: 3" (entire content) -->

<!-- aria-atomic="false" - Announce only changed content (default) -->
<div aria-live="polite" aria-atomic="false">
    <span>Items in cart:</span>
    <span id="cart-count">3</span>
</div>
<!-- Announces: "3" (only changed part) -->

<!-- aria-relevant - What changes to announce -->
<div aria-live="polite"
     aria-atomic="false"
     aria-relevant="additions removals text">
    <ul id="notification-list"></ul>
</div>

aria-relevant values:

  • additions - Announce when nodes added
  • removals - Announce when nodes removed
  • text - Announce text changes
  • all - Announce all changes (default)

ARIA in Modern Frameworks {#modern-frameworks}

React ARIA Patterns

Accessible Modal Component:

import { useEffect, useRef } from 'react';

function AccessibleModal({ isOpen, onClose, title, children }) {
    const dialogRef = useRef(null);
    const previousFocus = useRef(null);

    // Focus trap and management
    useEffect(() => {
        if (isOpen) {
            // Store current focus
            previousFocus.current = document.activeElement;

            // Focus dialog
            dialogRef.current?.focus();

            // Prevent body scroll
            document.body.style.overflow = 'hidden';

            return () => {
                // Restore focus and scroll
                previousFocus.current?.focus();
                document.body.style.overflow = '';
            };
        }
    }, [isOpen]);

    // Close on Escape key
    useEffect(() => {
        const handleEscape = (e) => {
            if (e.key === 'Escape' && isOpen) {
                onClose();
            }
        };

        document.addEventListener('keydown', handleEscape);
        return () => document.removeEventListener('keydown', handleEscape);
    }, [isOpen, onClose]);

    if (!isOpen) return null;

    return (
        <div className="modal-overlay" onClick={onClose}>
            <div
                ref={dialogRef}
                role="dialog"
                aria-modal="true"
                aria-labelledby="dialog-title"
                aria-describedby="dialog-desc"
                tabIndex={-1}
                className="modal-dialog"
                onClick={(e) => e.stopPropagation()}
            >
                <div className="modal-header">
                    <h2 id="dialog-title">{title}</h2>
                    <button
                        onClick={onClose}
                        aria-label="Close dialog"
                        className="close-btn"
                    >
                        ×
                    </button>
                </div>

                <div id="dialog-desc" className="modal-body">
                    {children}
                </div>

                <div className="modal-footer">
                    <button onClick={onClose} className="btn-primary">
                        Confirm
                    </button>
                    <button onClick={onClose} className="btn-secondary">
                        Cancel
                    </button>
                </div>
            </div>
        </div>
    );
}

// Usage
function App() {
    const [showModal, setShowModal] = useState(false);

    return (
        <>
            <button onClick={() => setShowModal(true)}>
                Open Modal
            </button>

            <AccessibleModal
                isOpen={showModal}
                onClose={() => setShowModal(false)}
                title="Confirm Action"
            >
                <p>Are you sure you want to proceed?</p>
            </AccessibleModal>
        </>
    );
}

Accessible Tab Component:

function AccessibleTabs({ tabs }) {
    const [activeTab, setActiveTab] = useState(0);

    const handleKeyDown = (e, index) => {
        let newIndex = index;

        switch (e.key) {
            case 'ArrowRight':
                newIndex = (index + 1) % tabs.length;
                break;
            case 'ArrowLeft':
                newIndex = (index - 1 + tabs.length) % tabs.length;
                break;
            case 'Home':
                newIndex = 0;
                break;
            case 'End':
                newIndex = tabs.length - 1;
                break;
            default:
                return;
        }

        e.preventDefault();
        setActiveTab(newIndex);
        document.getElementById(`tab-${newIndex}`)?.focus();
    };

    return (
        <div className="tabs">
            <div role="tablist" aria-label="Product information">
                {tabs.map((tab, index) => (
                    <button
                        key={index}
                        id={`tab-${index}`}
                        role="tab"
                        aria-selected={activeTab === index}
                        aria-controls={`panel-${index}`}
                        tabIndex={activeTab === index ? 0 : -1}
                        onClick={() => setActiveTab(index)}
                        onKeyDown={(e) => handleKeyDown(e, index)}
                    >
                        {tab.label}
                    </button>
                ))}
            </div>

            {tabs.map((tab, index) => (
                <div
                    key={index}
                    id={`panel-${index}`}
                    role="tabpanel"
                    aria-labelledby={`tab-${index}`}
                    hidden={activeTab !== index}
                    tabIndex={0}
                >
                    {tab.content}
                </div>
            ))}
        </div>
    );
}

// Usage
<AccessibleTabs
    tabs={[
        { label: 'Description', content: <p>Product description...</p> },
        { label: 'Specifications', content: <p>Product specs...</p> },
        { label: 'Reviews', content: <p>Customer reviews...</p> }
    ]}
/>

Common ARIA Mistakes to Avoid {#common-mistakes}

Mistake #1: Using ARIA on Native HTML

Wrong:

<button role="button" aria-label="Submit">Submit</button>

Right:

<button>Submit</button>

Native <button> already has button semantics. Adding role="button" is redundant.


Mistake #2: Breaking Keyboard Accessibility

Wrong:

<div role="button" aria-label="Click me">Click Me</div>

Right:

<div role="button" tabindex="0" aria-label="Click me"
     onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') handleClick(); }}>
    Click Me
</div>

Or better yet, use <button>:

<button>Click Me</button>

When using ARIA roles that imply interactivity, you MUST:

  1. Add tabindex="0" to make it focusable
  2. Handle keyboard events (Enter and Space for buttons)
  3. Provide visible focus indicators

Mistake #3: Conflicting Labels

Wrong:

<button aria-label="Submit form">
    Cancel
</button>

Screen reader announces "Submit form" but visible text says "Cancel". Confusing!

Right:

<button aria-label="Cancel form submission">
    Cancel
</button>

Or better:

<button>Cancel</button>

WCAG 2.5.3 Label in Name requires that the accessible name contains the visible text.


Mistake #4: Empty aria-label

Wrong:

<button aria-label="">
    <svg><!-- Icon --></svg>
</button>

Empty aria-label removes all accessible text, making button completely inaccessible.

Right:

<button aria-label="Close">
    <svg><!-- Icon --></svg>
</button>

Mistake #5: Overusing aria-hidden

Wrong:

<div aria-hidden="true">
    <h1>Important Heading</h1>
    <p>Important content...</p>
    <button>Important Action</button>
</div>

This hides ALL content from screen readers, including interactive elements.

Right (hide decorative elements only):

<button aria-label="Delete item">
    <span aria-hidden="true">🗑</span> Delete
</button>

Never use aria-hidden="true" on:

  • Focusable elements
  • Their container elements
  • Content that conveys meaning

Mistake #6: Missing aria-live Announcements

Wrong:

<div id="status"></div>

<script>
// Change is silent to screen readers
document.getElementById('status').textContent = 'Form saved!';
</script>

Right:

<div role="status" aria-live="polite" aria-atomic="true">
    <span id="status"></span>
</div>

<script>
// Announces "Form saved!" to screen readers
document.getElementById('status').textContent = 'Form saved!';
</script>

Mistake #7: Incorrect ARIA Roles

Wrong:

<div role="navigation">
    <button>Click me</button>
</div>

Navigation role should contain links, not buttons.

Right:

<nav role="navigation" aria-label="Main navigation">
    <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
    </ul>
</nav>

Testing ARIA Implementation {#testing}

Automated Testing Tools

1. axe DevTools (Browser Extension)

# Install axe DevTools for Chrome/Firefox/Edge
# Run automated WCAG audit
# Checks for ARIA attribute errors, missing labels, and more

2. WAVE (Web Accessibility Evaluation Tool)

# Install WAVE browser extension
# Visual feedback on accessibility issues
# Highlights ARIA roles, labels, and errors

3. Lighthouse (Chrome DevTools)

// Run in Chrome DevTools > Lighthouse
// Select "Accessibility" category
// Checks ARIA attributes and roles

4. Automated test in CI/CD:

// axe-core in Jest tests
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

test('Modal has no accessibility violations', async () => {
    const { container } = render(<AccessibleModal isOpen={true} />);
    const results = await axe(container);
    expect(results).toHaveNoViolations();
});

Manual Testing with Screen Readers

1. NVDA (Windows) - Free

Download: https://www.nvaccess.org/download/
Keyboard shortcuts:
- NVDA + Down Arrow: Read next item
- NVDA + F7: List all headings/landmarks/links
- NVDA + Space: Activate element
- Tab: Navigate interactive elements

2. JAWS (Windows) - Commercial

Most popular commercial screen reader
Keyboard shortcuts:
- Down Arrow: Next line
- Insert + F7: List of links
- Insert + F6: List of headings
- Insert + Ctrl + ; : List of ARIA landmarks

3. VoiceOver (macOS/iOS) - Built-in

Enable: System Preferences > Accessibility > VoiceOver
Keyboard shortcuts:
- VO + Right Arrow: Next item (VO = Ctrl + Option)
- VO + U: Rotor (lists headings, links, landmarks)
- VO + Space: Activate element
- Tab: Navigate interactive elements

4. TalkBack (Android) - Built-in

Enable: Settings > Accessibility > TalkBack
Gestures:
- Swipe right: Next item
- Swipe left: Previous item
- Double tap: Activate

ARIA Testing Checklist

□ All interactive elements have accessible names
  - Test: Tab through page, verify each element announces purpose

□ ARIA roles match actual behavior
  - Test: Activate custom widgets with keyboard and screen reader

□ aria-expanded reflects actual state
  - Test: Open/close accordions/menus, verify state announcement

□ aria-live regions announce updates
  - Test: Trigger dynamic content, verify announcements

□ Focus management works correctly
  - Test: Open modal, verify focus moves to dialog
  - Test: Close modal, verify focus returns to trigger

□ Keyboard navigation follows expected patterns
  - Test: Arrow keys navigate tabs, lists, menus
  - Test: Escape closes dialogs and menus

□ No conflicting labels (visible vs accessible)
  - Test: Compare visible text with screen reader announcement

□ aria-hidden not on focusable elements
  - Test: Tab through page, verify no hidden focusable elements

□ Required form fields marked with aria-required
  - Test: Screen reader announces "required" for mandatory fields

□ Form errors associated with fields
  - Test: Submit invalid form, verify errors announced and associated

ARIA Best Practices 2025 {#best-practices}

1. Use Semantic HTML First

✅ DO:

<button>Click me</button>
<a href="/page">Link</a>
<nav><ul><li><a href="/">Home</a></li></ul></nav>

❌ DON'T:

<div role="button" tabindex="0">Click me</div>
<span role="link">Link</span>
<div role="navigation"><div role="list"><div role="listitem">...</div></div></div>

2. Label All Interactive Elements

✅ DO:

<button aria-label="Close modal">×</button>
<input type="search" aria-label="Search products">

❌ DON'T:

<button>×</button>  <!-- Screen reader says "times button" -->
<input type="search" placeholder="Search">  <!-- Placeholder ≠ label -->

3. Manage Focus Properly

✅ DO:

// Open modal - move focus to dialog
dialogElement.focus();

// Close modal - return focus to trigger
triggerButton.focus();

❌ DON'T:

// Open modal - leave focus on background
// User doesn't know modal opened

// Close modal - focus lost
// Keyboard user doesn't know where they are

4. Announce Dynamic Changes

✅ DO:

<div role="status" aria-live="polite">
    <span id="cart-status">Item added to cart</span>
</div>

❌ DON'T:

<div id="cart-status">Item added to cart</div>
<!-- Silent update, screen reader user doesn't know -->

5. Keep Accessible Name Consistent with Visible Text

✅ DO:

<button>Submit Form</button>
<!-- OR -->
<button aria-label="Submit contact form">Submit</button>
<!-- Accessible name contains visible text "Submit" -->

❌ DON'T:

<button aria-label="Send message">Submit</button>
<!-- Voice control users say "Click Submit" - won't work -->

WCAG 2.5.3 Label in Name requires accessible name contains visible text.


6. Don't Rely on ARIA Alone

✅ DO:

<button disabled aria-disabled="true">Submit</button>
<!-- Native disabled + ARIA for maximum compatibility -->

❌ DON'T:

<button aria-disabled="true">Submit</button>
<!-- ARIA alone doesn't prevent click events -->

7. Test with Real Users

✅ DO:

  • Test with screen reader users
  • Test with keyboard-only users
  • Test with voice control users
  • Get feedback from disability community

❌ DON'T:

  • Rely only on automated tools
  • Assume compliance without user testing
  • Ignore user feedback

Conclusion

ARIA is a powerful tool for web accessibility, but it requires understanding and careful implementation. In 2025, with WCAG 2.2 as the legal standard and 4,605 ADA lawsuits targeting accessibility violations, proper ARIA implementation is both a legal necessity and ethical imperative.

Key Takeaways

  1. Semantic HTML first - Use native elements before ARIA
  2. Label everything - All interactive elements need accessible names
  3. Roles define structure - Use landmark and widget roles appropriately
  4. States communicate changes - Update aria-expanded, aria-selected, etc.
  5. Announce updates - Use aria-live regions for dynamic content
  6. Test thoroughly - Automated tools + manual screen reader testing
  7. Keep learning - ARIA specifications evolve, stay current

Resources

Official Specifications:

Testing Tools:

Need help with ARIA implementation?

Try AllAccessible →

Our automated solution provides:

  • Real-time ARIA remediation
  • Missing label detection and fixes
  • Live region implementation
  • Comprehensive WCAG 2.2 compliance

Last updated: November 1, 2025 | WCAG 2.2 and ARIA 1.3 compliant guide

Share this article