Blog

ARIA Best Practices: Accessible Rich Internet Applications Guide

TestParty
TestParty
July 24, 2025

ARIA (Accessible Rich Internet Applications) extends HTML semantics for complex web applications that native elements can't fully describe. ARIA enables accessible custom widgets, dynamic content, and single-page applications—but misused ARIA is worse than no ARIA. The first rule of ARIA: don't use ARIA if a native HTML element works.

Understanding when and how to use ARIA is essential for modern web accessibility. This guide covers the fundamentals, common patterns, and mistakes to avoid—helping you write ARIA that actually helps screen reader users.

Q: When should I use ARIA?

A: Use ARIA when native HTML elements can't express the semantics you need. Custom widgets (tabs, accordions, comboboxes), dynamic content updates, and complex relationships require ARIA. If a native element works (<button>, <nav>, <dialog>), use it instead—native semantics are more reliable than ARIA.

ARIA Fundamentals

What ARIA Does

ARIA modifies how elements appear in the accessibility tree—the structured representation screen readers use to understand and navigate web content. ARIA can:

  • Add roles: Identify what an element is (button, tab, dialog)
  • Express states: Indicate current state (expanded, selected, pressed)
  • Define properties: Provide additional information (labels, descriptions, relationships)

What ARIA Doesn't Do

ARIA doesn't change:

  • Visual appearance
  • Keyboard behavior
  • Element functionality

Critical understanding: role="button" doesn't make a <div> act like a button. It tells screen readers the div is supposed to be a button. You must still implement keyboard handling, focus management, and click behavior yourself.

The Five Rules of ARIA

1. Don't use ARIA if native HTML works

<!-- Wrong: ARIA on element that works natively -->
<div role="button" tabindex="0">Click me</div>

<!-- Right: Native element -->
<button>Click me</button>

2. Don't change native semantics unnecessarily

<!-- Wrong: Overriding heading semantics -->
<h2 role="tab">Tab name</h2>

<!-- Right: Appropriate element for role -->
<div role="tab">Tab name</div>

3. All interactive ARIA controls must be keyboard accessible

// If you add role="button", you must handle Enter/Space
element.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    element.click();
  }
});

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

<!-- Wrong: Hidden but focusable -->
<button aria-hidden="true">Can still focus this</button>

<!-- Right: Actually remove from interaction -->
<button hidden>Not focusable</button>

5. All interactive elements must have an accessible name

<!-- Wrong: No accessible name -->
<button><svg>...</svg></button>

<!-- Right: Accessible name provided -->
<button aria-label="Close"><svg>...</svg></button>

Common ARIA Roles

Landmark Roles

Landmarks help users navigate page structure:

<header role="banner">          <!-- Use <header> instead when possible -->
<nav role="navigation">         <!-- Use <nav> instead -->
<main role="main">              <!-- Use <main> instead -->
<footer role="contentinfo">     <!-- Use <footer> instead -->
<aside role="complementary">    <!-- Use <aside> instead -->
<form role="search">            <!-- No native equivalent for search -->
<section role="region">         <!-- Use <section> with aria-label -->

Best practice: Use native HTML5 elements. They provide landmarks automatically:

<header>...</header>        <!-- Implicit role="banner" -->
<nav>...</nav>              <!-- Implicit role="navigation" -->
<main>...</main>            <!-- Implicit role="main" -->
<footer>...</footer>        <!-- Implicit role="contentinfo" -->

Widget Roles

For custom interactive components:

<!-- Tabs -->
<div role="tablist">
  <button role="tab" aria-selected="true">Tab 1</button>
  <button role="tab" aria-selected="false">Tab 2</button>
</div>
<div role="tabpanel">Tab 1 content</div>

<!-- Modal dialog -->
<div role="dialog" aria-modal="true" aria-labelledby="title">
  <h2 id="title">Dialog Title</h2>
</div>

<!-- Alert -->
<div role="alert">Important message</div>

<!-- Menu -->
<ul role="menu">
  <li role="menuitem">Option 1</li>
  <li role="menuitem">Option 2</li>
</ul>

Document Structure Roles

For content organization:

<!-- List (usually implicit from <ul>/<ol>) -->
<div role="list">
  <div role="listitem">Item 1</div>
  <div role="listitem">Item 2</div>
</div>

<!-- Table (usually implicit from <table>) -->
<div role="table">
  <div role="row">
    <div role="columnheader">Header</div>
  </div>
  <div role="row">
    <div role="cell">Data</div>
  </div>
</div>

Best practice: Use native <ul>, <ol>, <table> elements. Role overrides are for CSS Grid layouts that are semantically tables/lists.

States and Properties

Common States

<!-- Expandable content -->
<button aria-expanded="false" aria-controls="content">
  Show more
</button>
<div id="content" hidden>Content</div>

<!-- Selected state -->
<li role="tab" aria-selected="true">Selected tab</li>

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

<!-- Disabled -->
<button aria-disabled="true">Can't click</button>

<!-- Invalid (form validation) -->
<input aria-invalid="true" aria-describedby="error">
<span id="error">Invalid email format</span>

<!-- Current (navigation) -->
<a href="/" aria-current="page">Home</a>

Important Properties

<!-- Label: Provides accessible name -->
<button aria-label="Close menu">Ă—</button>

<!-- Labelledby: References visible text -->
<div role="dialog" aria-labelledby="dialog-title">
  <h2 id="dialog-title">Confirmation</h2>
</div>

<!-- Describedby: Additional information -->
<input aria-describedby="password-hint">
<span id="password-hint">Must be 8+ characters</span>

<!-- Controls: Identifies controlled element -->
<button aria-controls="dropdown" aria-expanded="false">
  Options
</button>
<ul id="dropdown" hidden>...</ul>

<!-- Live: Announces dynamic changes -->
<div aria-live="polite">Status updates appear here</div>

<!-- Hidden: Removes from accessibility tree -->
<div aria-hidden="true">Decorative content</div>

ARIA Patterns

Tabs

<div class="tabs">
  <div role="tablist" aria-label="Product information">
    <button role="tab"
            id="tab-1"
            aria-selected="true"
            aria-controls="panel-1">
      Description
    </button>
    <button role="tab"
            id="tab-2"
            aria-selected="false"
            aria-controls="panel-2"
            tabindex="-1">
      Specifications
    </button>
    <button role="tab"
            id="tab-3"
            aria-selected="false"
            aria-controls="panel-3"
            tabindex="-1">
      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>
    Specifications content...
  </div>

  <div role="tabpanel"
       id="panel-3"
       aria-labelledby="tab-3"
       hidden>
    Reviews content...
  </div>
</div>

Keyboard handling:

  • Arrow keys move between tabs
  • Only selected tab is in tab order
  • Tab key moves to panel content

Accordion

<div class="accordion">
  <h3>
    <button aria-expanded="true"
            aria-controls="section-1">
      Section 1
    </button>
  </h3>
  <div id="section-1">
    Section 1 content...
  </div>

  <h3>
    <button aria-expanded="false"
            aria-controls="section-2">
      Section 2
    </button>
  </h3>
  <div id="section-2" hidden>
    Section 2 content...
  </div>
</div>

Note: Native <details> and <summary> elements provide accordion behavior without ARIA.

Modal Dialog

<div role="dialog"
     aria-modal="true"
     aria-labelledby="dialog-title"
     aria-describedby="dialog-desc">
  <h2 id="dialog-title">Confirm Delete</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>

Requirements:

  • Focus trapped within dialog
  • Escape closes dialog
  • Focus returns to trigger on close
  • Background content has aria-hidden="true" or inert

Better approach: Use native <dialog> element where supported:

<dialog aria-labelledby="dialog-title">
  <h2 id="dialog-title">Confirm Delete</h2>
  ...
</dialog>

Live Regions

For dynamic content that should be announced:

<!-- Polite: Announces when convenient (most cases) -->
<div aria-live="polite">
  Status message appears here
</div>

<!-- Assertive: Interrupts immediately (urgent only) -->
<div role="alert" aria-live="assertive">
  Error message
</div>

<!-- Status: Implicitly polite, for status messages -->
<div role="status">
  3 items in cart
</div>

Usage:

// Element must exist before content changes
const status = document.getElementById('status');
status.textContent = 'Form submitted successfully';
// Screen reader announces the new content

Combobox (Autocomplete)

<div class="combobox-wrapper">
  <label id="search-label">Search products</label>
  <input type="text"
         role="combobox"
         aria-labelledby="search-label"
         aria-expanded="false"
         aria-autocomplete="list"
         aria-controls="search-listbox"
         aria-activedescendant="">

  <ul id="search-listbox"
      role="listbox"
      aria-labelledby="search-label"
      hidden>
    <li role="option" id="opt-1">Running shoes</li>
    <li role="option" id="opt-2">Running shorts</li>
    <li role="option" id="opt-3">Running socks</li>
  </ul>
</div>

Keyboard handling:

  • Typing filters options
  • Arrow keys navigate options
  • Enter selects highlighted option
  • Escape closes listbox
  • aria-activedescendant points to current option

Common Mistakes

Redundant ARIA

<!-- Wrong: Redundant role on native element -->
<button role="button">Submit</button>
<nav role="navigation">...</nav>

<!-- Right: Let native semantics work -->
<button>Submit</button>
<nav>...</nav>

ARIA Without Keyboard Support

<!-- Wrong: ARIA claims it's a button, but no keyboard handling -->
<span role="button" onclick="doThing()">Click</span>

<!-- Right: Full keyboard support -->
<span role="button"
      tabindex="0"
      onclick="doThing()"
      onkeydown="if(event.key==='Enter'||event.key===' ')doThing()">
  Click
</span>

<!-- Better: Just use a button -->
<button onclick="doThing()">Click</button>

Incorrect State Management

<!-- Wrong: Static ARIA that doesn't update -->
<button aria-expanded="false" onclick="toggleMenu()">Menu</button>

<!-- Right: ARIA updates with state -->
<script>
function toggleMenu() {
  const button = document.querySelector('button');
  const expanded = button.getAttribute('aria-expanded') === 'true';
  button.setAttribute('aria-expanded', !expanded);
  // Also toggle menu visibility
}
</script>

aria-hidden on Interactive Elements

<!-- Wrong: Hidden but interactive -->
<div aria-hidden="true">
  <button>This button can still receive focus!</button>
</div>

<!-- Right: Use inert or actually hide -->
<div inert>
  <button>Can't focus this</button>
</div>

Missing Accessible Names

<!-- Wrong: No accessible name -->
<button><svg class="icon-close"></svg></button>
<input type="search">

<!-- Right: Accessible names provided -->
<button aria-label="Close dialog">
  <svg class="icon-close" aria-hidden="true"></svg>
</button>
<input type="search" aria-label="Search products">

Testing ARIA

Browser Developer Tools

Chrome/Edge:

  • Inspect element → Accessibility tab
  • Shows computed accessible name, role, state

Firefox:

  • Inspect → Accessibility tab
  • Full accessibility tree view

Screen Reader Testing

The definitive test is actual screen reader usage:

NVDA (Windows):

  • Navigate with arrow keys and Tab
  • Listen to role, name, state announcements
  • Verify live regions announce updates

VoiceOver (Mac/iOS):

  • Use VO+arrow keys for navigation
  • Verify rotor shows correct element types
  • Test on iOS for touch-based ARIA

Automated Testing

TestParty detects common ARIA issues:

  • Invalid ARIA attributes
  • Missing required properties
  • Incorrect role usage
  • Accessible name problems

For e-commerce sites, this catches custom widget issues in product selectors, filters, and checkout flows.

FAQ Section

Q: Is ARIA required for WCAG compliance?

A: ARIA isn't required per se, but WCAG compliance requires accessible semantics. If native HTML provides required semantics, ARIA is unnecessary. If you need semantics beyond native HTML, ARIA is required.

Q: Does ARIA work on mobile devices?

A: Yes. VoiceOver (iOS) and TalkBack (Android) interpret ARIA. However, some ARIA patterns work better than others on touch interfaces—test critical widgets on mobile screen readers.

Q: How do I choose between aria-label and aria-labelledby?

A: Use aria-labelledby when visible text already labels the element. Use aria-label when no visible text exists or when the visible text doesn't fully describe the element.

Q: Why doesn't my aria-live region work?

A: Live regions must exist in the DOM before content changes. Adding new content to an existing live region triggers announcement. Creating a new element with aria-live doesn't work reliably.

Q: Can I use multiple ARIA roles?

A: No. Elements can have only one role. If you need multiple semantic meanings, restructure your HTML or use additional ARIA properties.

Key Takeaways

  • Prefer native HTML over ARIA. Buttons, links, landmarks—native elements are more reliable than ARIA recreations.
  • ARIA changes accessibility tree, not behavior. Adding role="button" doesn't create button functionality—you must implement it yourself.
  • Keep states synchronized. aria-expanded, aria-selected, aria-pressed must update with visual state changes.
  • Every interactive element needs an accessible name. Use visible labels, aria-label, or aria-labelledby.
  • Test with screen readers. Automated tools catch ARIA syntax errors; screen readers reveal actual usability.
  • Follow established patterns. WAI-ARIA Authoring Practices provides tested patterns for complex widgets.

Conclusion

ARIA is powerful but dangerous—incorrect ARIA makes accessibility worse, not better. The path to correct ARIA usage starts with understanding native HTML semantics and using them whenever possible.

When custom widgets require ARIA, follow established patterns. Tabs, accordions, dialogs, and comboboxes have documented implementations in the WAI-ARIA Authoring Practices. Don't invent new patterns when proven ones exist.

TestParty identifies ARIA issues and provides fixes following best practices. For e-commerce sites, this means accessible product filters, quantity selectors, and checkout widgets that actually work with screen readers.

Ready to fix your ARIA implementation? Get a free accessibility scan to identify ARIA issues across your site.


Related Articles:


Content disclosure: This article was produced using AI-assisted tools and reviewed by TestParty's team of accessibility specialists. As a company focused on source code remediation and continuous accessibility monitoring, we aim to share practical knowledge about WCAG and ADA compliance. That said, accessibility is complex and context-dependent. The information here is educational only—please work with qualified professionals for guidance specific to your organization's needs.

Stay informed

Accessibility insights delivered
straight to your inbox.

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