Skip to content
ARIA: Accessible Rich Internet Applications Guide

ARIA: Accessible Rich Internet Applications Guide

DodaTech Updated Jun 20, 2026 10 min read

ARIA (Accessible Rich Internet Applications) is a W3C specification that supplements HTML with additional roles, states, and properties — making dynamic content and custom widgets accessible to assistive technologies when native HTML isn’t enough.

What You’ll Learn

By the end of this tutorial, you’ll understand ARIA roles (landmark, widget, document), states and properties (aria-label, aria-describedby, aria-expanded, aria-hidden), live regions for dynamic content, when to use ARIA vs native HTML, and how to build common accessible patterns like tabs, accordions, and modals.

Why ARIA Matters

Heather’s Law, a well-known accessibility adage, states: “No ARIA is better than bad ARIA.” ARIA is powerful but dangerous when misused. Used correctly, ARIA makes complex web applications usable by screen reader users. Used incorrectly, it can make your site less accessible. At DodaTech, Doda Browser’s built-in accessibility tools validate ARIA usage, helping developers catch misuse during development.

ARIA Learning Path

    flowchart LR
  A[Accessibility Overview] --> B[WCAG Compliance]
  B --> C[ARIA Basics]
  C --> D[Keyboard Navigation]
  C --> E[Screen Readers]
  C:::current

  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
  
Prerequisites: HTML basics, understanding of WCAG POUR principles. Familiarity with JavaScript for the interactive patterns section.

The First Rule of ARIA

Before using ARIA, ask yourself: Can I use a native HTML element instead?

<!-- ❌ Bad: Using ARIA on a div to make a button -->
<div role="button" tabindex="0" onclick="submitForm()">
  Submit
</div>

<!-- ✅ Good: Just use a native button -->
<button onclick="submitForm()">
  Submit
</button>

Native HTML elements have built-in keyboard support, focus management, and accessibility mappings. A <button> is automatically focusable, activatable with Enter/Space, and announced as a button by screen readers. A <div role="button"> requires manual implementation of all of this.

The ARIA rule of thumb: Use native HTML whenever possible. Only use ARIA when the native semantics don’t exist or can’t convey the necessary information.

ARIA Roles

ARIA roles define what an element is or does. They fall into three categories:

Landmark Roles

Landmark roles identify major sections of a page, enabling screen reader users to navigate quickly:

RoleNative HTML AlternativePurpose
role="banner"<header> (in body context)Site-wide branding
role="navigation"<nav>Navigation links
role="main"<main>Primary content
role="complementary"<aside>Supporting content
role="contentinfo"<footer> (in body context)Footer information
role="form"<form>Form container
role="region"<section> with labelGeneric landmark
<!-- Using native HTML5 landmarks (preferred) -->
<header>
  <nav aria-label="Main">
    <ul><!-- nav links --></ul>
  </nav>
</header>

<main>
  <h1>Page Title</h1>
  <section aria-labelledby="section-heading">
    <h2 id="section-heading">Section Title</h2>
    <p>Content here...</p>
  </section>
</main>

<footer>
  <p>&copy; 2026 DodaTech</p>
</footer>

Widget Roles

Widget roles define interactive controls:

RoleNative AlternativePurpose
role="button"<button>, <input type="button">Clickable control
role="link"<a href="...">Navigational link
role="tab"none nativeTab in a tab list
role="tabpanel"none nativeContent panel for a tab
role="dialog"<dialog>Modal or non-modal dialog
role="alertdialog"none nativeUrgent dialog (with alert role)
<!-- Custom tab widget using ARIA -->
<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">
    Specifications
  </button>
  <button role="tab" aria-selected="false" aria-controls="panel-3" id="tab-3">
    Reviews
  </button>
</div>

<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  <p>Product description content...</p>
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  <p>Technical specifications...</p>
</div>
<div role="tabpanel" id="panel-3" aria-labelledby="tab-3" hidden>
  <p>Customer reviews...</p>
</div>

Document Structure Roles

These describe the structure of content within a page:

RolePurpose
role="heading"Use <h1>-<h6> instead
role="list"Use <ul> or <ol> instead
role="listitem"Use <li> instead
role="img"Use <img> with alt instead
role="presentation"Removes semantic meaning
role="none"Same as presentation

ARIA States and Properties

ARIA states (dynamic, changeable) and properties (static, descriptive) provide additional information about elements:

Labels and Descriptions

<!-- aria-label — overrides visible text -->
<button aria-label="Close dialog" onclick="closeDialog()">×</button>

<!-- aria-labelledby — references another element's text -->
<h2 id="dialog-title">Confirm Deletion</h2>
<p id="dialog-desc">Are you sure you want to delete your account?</p>
<div role="dialog" aria-labelledby="dialog-title" aria-describedby="dialog-desc">
  <button onclick="confirmDelete()">Delete</button>
  <button onclick="cancelDelete()">Cancel</button>
</div>

<!-- aria-describedby — provides additional context -->
<label for="password">Password</label>
<input type="password" id="password"
       aria-describedby="password-hint">
<p id="password-hint">Must be at least 8 characters with a number</p>

Live Regions

Live regions announce content changes to screen readers without moving focus:

<!-- aria-live="polite" — announces when idle -->
<div aria-live="polite" aria-atomic="true">
  Cart: 3 items
</div>

<!-- aria-live="assertive" — interrupts immediately (use sparingly) -->
<div role="alert">
  Error: Failed to save changes
</div>

<!-- aria-relevant — what changes trigger announcements -->
<!-- "additions" (default), "removals", "text", "all" -->
<ul aria-live="polite" aria-relevant="additions" id="notification-list">
  <!-- new notifications appear here -->
</ul>

Common States

<!-- aria-expanded — for accordions, menus, disclosures -->
<button aria-expanded="false" aria-controls="section-1">
  More Information
</button>
<div id="section-1" hidden>
  <p>Hidden content revealed when expanded...</p>
</div>

<!-- aria-hidden — hides decorative elements from AT -->
<span aria-hidden="true"></span> Next Page

<!-- aria-current — indicates current item in a set -->
<nav aria-label="Breadcrumb">
  <ol>
    <li><a href="/">Home</a></li>
    <li><a href="/products">Products</a></li>
    <li><a href="/products/laptop" aria-current="page">Laptop</a></li>
  </ol>
</nav>

<!-- aria-disabled — disabled state that works with all elements -->
<button aria-disabled="true">Submit</button>

When to Use ARIA vs Native HTML

ScenarioUse NativeUse ARIA
Button<button>role="button" on <div> (last resort)
Navigation<nav>role="navigation" on <div>
Modal dialog<dialog>role="dialog" on <div> (with JS)
Tabsnone nativerole="tablist", role="tab", role="tabpanel"
Progress bar<progress>role="progressbar" for custom
Alertnone nativerole="alert" on <div> with JS
Tooltiptitle attributearia-describedby plus custom tooltip

ARIA Patterns: Step by Step

Accessible Tabs

<div role="tablist" aria-label="Documentation">
  <button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
    Getting Started
  </button>
  <button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2">
    API Reference
  </button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  <h2>Getting Started</h2>
  <p>Content for the first tab...</p>
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  <h2>API Reference</h2>
  <p>Content for the second tab...</p>
</div>

<script>
function initTabs() {
  const tabs = document.querySelectorAll('[role="tab"]');
  const panels = document.querySelectorAll('[role="tabpanel"]');

  tabs.forEach(tab => {
    tab.addEventListener('click', () => activateTab(tab));
    tab.addEventListener('keydown', (e) => {
      const tabList = e.target.closest('[role="tablist"]');
      const tabs = [...tabList.querySelectorAll('[role="tab"]')];
      const idx = tabs.indexOf(e.target);

      if (e.key === 'ArrowRight' && idx < tabs.length - 1) {
        activateTab(tabs[idx + 1]);
        tabs[idx + 1].focus();
      } else if (e.key === 'ArrowLeft' && idx > 0) {
        activateTab(tabs[idx - 1]);
        tabs[idx - 1].focus();
      }
    });
  });

  function activateTab(tab) {
    // Deselect all
    tabs.forEach(t => {
      t.setAttribute('aria-selected', 'false');
    });
    panels.forEach(p => p.hidden = true);

    // Select target
    tab.setAttribute('aria-selected', 'true');
    const panel = document.getElementById(tab.getAttribute('aria-controls'));
    if (panel) panel.hidden = false;
  }
}

initTabs();
</script>

Keyboard behavior: Left/Right arrows switch tabs. Tab enters the active tab panel. This follows the WAI-ARIA Authoring Practices.

Accessible Accordion

<div>
  <h3>
    <button aria-expanded="false" aria-controls="faq-1" id="faq-btn-1">
      What is WCAG?
    </button>
  </h3>
  <div id="faq-1" role="region" aria-labelledby="faq-btn-1" hidden>
    <p>WCAG stands for Web Content Accessibility Guidelines...</p>
  </div>

  <h3>
    <button aria-expanded="false" aria-controls="faq-2" id="faq-btn-2">
      What is ARIA?
    </button>
  </h3>
  <div id="faq-2" role="region" aria-labelledby="faq-btn-2" hidden>
    <p>ARIA stands for Accessible Rich Internet Applications...</p>
  </div>
</div>

<script>
function initAccordion() {
  document.querySelectorAll('[aria-expanded]').forEach(btn => {
    btn.addEventListener('click', () => {
      const expanded = btn.getAttribute('aria-expanded') === 'true';
      btn.setAttribute('aria-expanded', !expanded);
      const panel = document.getElementById(btn.getAttribute('aria-controls'));
      if (panel) panel.hidden = expanded;
    });
  });
}
initAccordion();
</script>

Accessible Modal Dialog

<button onclick="openModal()">Open Settings</button>

<div role="dialog"
     aria-modal="true"
     aria-labelledby="modal-title"
     id="settings-modal"
     hidden>
  <div role="document">
    <h2 id="modal-title">Settings</h2>
    <p>Configure your preferences below.</p>

    <label for="theme">Theme</label>
    <select id="theme">
      <option>Light</option>
      <option>Dark</option>
    </select>

    <button onclick="saveSettings()">Save</button>
    <button onclick="closeModal()" aria-label="Close settings">×</button>
  </div>
</div>

<div id="modal-backdrop" hidden></div>

<script>
function openModal() {
  const modal = document.getElementById('settings-modal');
  const backdrop = document.getElementById('modal-backdrop');
  modal.hidden = false;
  backdrop.hidden = false;
  // Focus first focusable element
  modal.querySelector('button, input, select').focus();
  // Focus trap (simplified — see keyboard navigation tutorial)
}

function closeModal() {
  document.getElementById('settings-modal').hidden = true;
  document.getElementById('modal-backdrop').hidden = true;
  document.querySelector('[onclick="openModal()"]').focus();
}
</script>

Testing ARIA

Always test ARIA with real screen readers. Tools help, but the only way to verify ARIA works is with the assistive technologies your users rely on:

// Automated ARIA validation with axe-core
const { axe } = require('axe-core');

async function validateARIA(html) {
  const { JSDOM } = require('jsdom');
  const { window } = new JSDOM(html);
  const results = await axe.run(window.document, {
    runOnly: ['aria']
  });

  console.log(`Found ${results.violations.length} ARIA violations:`);
  results.violations.forEach(v => {
    console.log(`\n[${v.impact}] ${v.help}`);
    console.log(`  ${v.description}`);
    v.nodes.forEach(n => {
      console.log(`  → ${n.target}`);
      console.log(`    Fix: ${n.failureSummary}`);
    });
  });
}

validateARIA(`
  <div role="button">Click me</div>
  <div role="tab">Tab 1</div>
  <div role="tabpanel">Content</div>
`);

Expected output:

Found 2 ARIA violations:

[critical] ARIA buttons must have discernible text
  Fix: Element does not have inner text that is visible to screen readers

[serious] ARIA dialog and alertdialog nodes must have an accessible name
  Fix: [role="tabpanel"] requires an accessible name via aria-labelledby

Common ARIA Mistakes

1. Using role="presentation" on Focusable Elements

If you add role="presentation" or role="none" to a focusable element, screen readers won’t announce it, but keyboard users can still focus on it — creating a trap.

2. Overusing role="alert"

role="alert" interrupts screen readers immediately. Only use it for time-sensitive, critical errors. For less urgent updates, use aria-live="polite".

3. Using aria-hidden="true" on Focusable Elements

An element with aria-hidden="true" that contains focusable children creates a trap. Keyboard users can focus invisible elements. Either remove the children or add tabindex="-1".

4. Forgetting aria-controls

When using aria-expanded, always pair it with aria-controls pointing to the controlled region. Without it, screen reader users don’t know what element will expand.

5. Using ARIA Instead of Native Semantics

<div role="navigation"> instead of <nav>, <span role="heading" aria-level="2"> instead of <h2>, <div role="list"> instead of <ul> — all unnecessary and potentially buggy.

6. Not Updating ARIA States Dynamically

Setting aria-expanded="false" in HTML but never updating it via JavaScript means screen readers always hear “collapsed” even when the content is shown.

7. Redundant ARIA

<main role="main"> — redundant. Native <main> already maps to the main role. Adding role="main" is unnecessary and can confuse some older screen readers.

Practice Questions

1. What is the first rule of ARIA?

Don’t use ARIA if you can use a native HTML element that provides the semantics and behavior you need.

2. What’s the difference between aria-label and aria-labelledby?

aria-label provides a label string directly. aria-labelledby references the ID of another element to use as the label. aria-labelledby takes precedence over aria-label.

3. What does aria-live="polite" do?

It tells screen readers to announce changes to the element’s content when the user is idle, without interrupting their current task.

4. Why is role="alert" different from aria-live="assertive"?

role="alert" is a live region that automatically maps to aria-live="assertive" and aria-atomic="true". It also carries semantic meaning (it’s an alert).

5. Challenge: Build an accessible custom checkbox using ARIA (role="checkbox", aria-checked). It should receive focus (Tab), toggle with Space, and announce state changes to screen readers.

Real-World Task

Audit a page on your site for ARIA usage. Using the browser’s accessibility tree (Chrome DevTools → Elements → Accessibility), check every ARIA attribute is correctly applied. Fix any redundant, missing, or incorrect ARIA.

FAQ

Can ARIA fix all accessibility issues?
No. ARIA only affects the accessibility tree (what screen readers hear). It doesn’t fix keyboard navigation, color contrast, focus management, or other non-ARIA accessibility issues.
Do screen readers support all ARIA roles?
Most modern screen readers support the most common roles, but esoteric or rarely-used roles may have inconsistent support. Always test with real screen readers.
What’s the accessibility tree?
The accessibility tree is a subset of the DOM that browsers expose to assistive technologies. ARIA modifies this tree. You can view it in Chrome DevTools under Elements → Accessibility.
Should I use ARIA on native HTML elements?
Rarely. Native HTML already has implicit ARIA semantics. Adding role="button" to a <button> is redundant. The main exception is when you need to override semantics (e.g., <a role="button"> for a link that acts like a button).
Does ARIA affect visual rendering?
No. ARIA has no visual effect whatsoever. It only affects the accessibility tree.

Try It Yourself

Let’s build an accessible custom rating widget:

<div class="rating" role="radiogroup" aria-label="Rate this product">
  <span role="radio" aria-checked="false" aria-label="1 star" tabindex="0"
        data-value="1" onclick="setRating(1)" onkeydown="handleRatingKey(event, 1)"></span>
  <span role="radio" aria-checked="false" aria-label="2 stars" tabindex="-1"
        data-value="2" onclick="setRating(2)" onkeydown="handleRatingKey(event, 2)"></span>
  <span role="radio" aria-checked="false" aria-label="3 stars" tabindex="-1"
        data-value="3" onclick="setRating(3)" onkeydown="handleRatingKey(event, 3)"></span>
  <span role="radio" aria-checked="false" aria-label="4 stars" tabindex="-1"
        data-value="4" onclick="setRating(4)" onkeydown="handleRatingKey(event, 4)"></span>
  <span role="radio" aria-checked="false" aria-label="5 stars" tabindex="-1"
        data-value="5" onclick="setRating(5)" onkeydown="handleRatingKey(event, 5)"></span>
</div>
<p aria-live="polite" id="rating-output">No rating selected</p>

<script>
let currentRating = 0;

function setRating(value) {
  currentRating = value;
  document.querySelectorAll('[role="radio"]').forEach((radio, index) => {
    const checked = index < value;
    radio.setAttribute('aria-checked', checked);
    radio.setAttribute('aria-label', `${index + 1} ${index === 0 ? 'star' : 'stars'}`);
  });
  document.getElementById('rating-output').textContent =
    `You rated this ${value} ${value === 1 ? 'star' : 'stars'}`;
}

function handleRatingKey(event, value) {
  if (event.key === ' ' || event.key === 'Enter') {
    event.preventDefault();
    setRating(value);
  }
}
</script>

Expected behavior: Screen reader announces “Rate this product, radiogroup” then “1 star, radio, not checked”. Arrow keys navigate between stars. Space selects a rating. The live region announces the selection.

What’s Next

Congratulations on completing this ARIA Basics tutorial! Here’s where to go from here:

  • Practice daily — Add proper ARIA to one component per day
  • Build a project — Create an accessible tab component from scratch
  • Explore related topics — Learn keyboard navigation next
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro