Skip to content

ARIA — Accessible Rich Internet Applications Complete Guide

DodaTech Updated 2026-06-21 11 min read

In this tutorial, you'll learn about ARIA. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

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 alone is not enough.

What You'll Learn

By the end of this complete guide, you'll understand all ARIA role categories (landmark, widget, document), every major State and property (aria-label, aria-describedby, aria-expanded, aria-hidden, aria-current, aria-live), live regions for dynamic content, when to reach for ARIA versus native HTML, and how to build common accessible patterns like tabs, accordions, modals, and tooltips from scratch.

Why ARIA Matters

The web today is built on dynamic components — tab panels, autocomplete dropdowns, modal dialogs, live content feeds. Native HTML was not designed for these patterns. ARIA bridges the gap by telling assistive technologies what these custom widgets are, what they do, and what State they are in. At DodaTech, Doda Browser's built-in Accessibility panel validates ARUA usage in real time, flagging redundant, missing, or malformed attributes before they reach production.

ARIA Learning Path

flowchart LR
  A[Accessibility Overview] --> B[WCAG Compliance]
  B --> C[ARIA Guide]
  C --> D[Keyboard Navigation]
  C --> E[Screen Reader Guide]
  C --> F[Accessible Forms]
  C:::current

  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px

{{< callout type="info" icon="sparkles" >}} Prerequisites: Understanding of HTML semantics and WCAG POUR principles. Basic JavaScript for interactive pattern examples. {{< /callout >}}

The First Rule of ARIA

Before you add a single ARIA attribute, ask yourself: can I use a native HTML element?

<!-- ❌ Bad: ARIA button on a div -->
<div role="button" tabindex="0" onclick="doSomething()">
  Click me
</div>

<!-- ✅ Good: just a native button -->
<button onclick="doSomething()">
  Click me
</button>

Native HTML elements come with built-in keyboard support, focus management, and Accessibility tree mappings. A <button> is automatically focusable, activatable with Enter or Space, and announced as "button" by screen readers. A <div role="button"> requires you to implement all of that manually.

The golden rule: use native HTML whenever it exists. Only reach for ARIA when native semantics are absent or insufficient.

ARIA Roles

ARIA roles tell assistive technologies what an element is. They fall into three categories:

Landmark Roles

Landmark roles identify major sections of a page, enabling screen reader users to jump between regions quickly. The table below maps each role to its native HTML alternative:

Role Native HTML Alternative Purpose
role="banner" <header> (page-level) Site-wide branding and navigation
role="navigation" <nav> Set of navigation links
role="main" <main> Primary content of the page
role="complementary" <aside> Supporting content related to main
role="contentinfo" <footer> (page-level) Footer information such as copyright
role="region" <section> with label Generic landmark, needs a label
role="form" <form> Form container
<!-- Proper landmark structure -->
<header>
  <nav aria-label="Main navigation">
    <ul>
      <li><a href="/">Home</a></li>
      <li><a href="/products">Products</a></li>
      <li><a href="/about">About</a></li>
    </ul>
  </nav>
</header>

<main>
  <h1>Welcome to DodaTech</h1>
  <section aria-labelledby="features-heading">
    <h2 id="features-heading">Features</h2>
    <p>Content about features...</p>
  </section>
</main>

<footer>
  <p>&copy; 2026 DodaTech. Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.</p>
</footer>

Widget Roles

Widget roles define interactive controls that the user can operate:

Role Native Alternative Purpose
role="button" <button>, <input type="button"> Clickable control
role="link" <a href="..."> Navigational link
role="tab" none native Tab in a tab list
role="tabpanel" none native Content panel for a tab
role="dialog" <dialog> Modal or non-modal dialog
role="alertdialog" none native Urgent dialog
role="progressbar" <progress> Progress indicator
role="slider" <input type="range"> Range input
role="switch" none native On-off toggle
role="treeitem" none native Item in a tree view

Document Structure Roles

These describe the structure of content. Most have native HTML equivalents that should be used instead:

Role Preferred Native Element
role="heading" <h1> through <h6>
role="list" <ul> or <ol>
role="listitem" <li>
role="img" <img> with alt text
role="presentation" or role="none" Used to remove semantics

ARIA States and Properties

States and properties provide additional context about elements. States change dynamically (like aria-expanded), while properties are typically static (like aria-label).

Labels and Descriptions

Every interactive element needs an accessible name. ARIA provides three ways to supply one:

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

<!-- aria-labelledby: references another element's text as the label -->
<h2 id="modal-title">Confirm Deletion</h2>
<p id="modal-desc">This action cannot be undone.</p>
<div role="dialog" aria-labelledby="modal-title" aria-describedby="modal-desc">
  <button onclick="deleteItem()">Delete</button>
  <button onclick="cancel()">Cancel</button>
</div>

<!-- aria-describedby: provides supplementary description -->
<label for="email">Email address</label>
<input type="email" id="email" aria-describedby="email-format" required>
<p id="email-format">Format: name@example.com</p>

Live Regions

Live regions tell screen readers to announce content changes without moving focus. This is essential for dynamic content:

<!-- aria-live="polite": announces when user is idle -->
<div aria-live="polite" aria-atomic="true" id="cart-status">
  Cart: 3 items
</div>

<!-- role="alert": interrupts immediately for critical messages -->
<div role="alert" id="error-banner">
  Connection lost. Your changes will be saved locally.
</div>

<!-- aria-relevant: specifies what kinds of changes trigger announcements -->
<ul aria-live="polite" aria-relevant="additions removals" id="notification-feed">
  <!-- notifications appear here -->
</ul>

Common States

<!-- aria-expanded for disclosures, accordions, menus -->
<button aria-expanded="false" aria-controls="details-panel">
  Show details
</button>
<div id="details-panel" hidden>
  <p>Hidden content here...</p>
</div>

<!-- aria-hidden for decorative or off-screen content -->
<span aria-hidden="true">→</span> Continue reading

<!-- aria-current for indicating the 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 for disabled State on non-native elements -->
<div role="button" aria-disabled="true" tabindex="-1">Submit</div>

ARIA Patterns: Step by Step

Accessible Tabs

<div role="tablist" aria-label="Product information">
  <button role="tab" aria-selected="true" aria-controls="desc-panel" id="tab-desc">
    Description
  </button>
  <button role="tab" aria-selected="false" aria-controls="spec-panel" id="tab-spec">
    Specifications
  </button>
  <button role="tab" aria-selected="false" aria-controls="review-panel" id="tab-review">
    Reviews
  </button>
</div>

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

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

  function activateTab(newTab) {
    tabs.forEach(t => t.setAttribute('aria-selected', 'false'));
    panels.forEach(p => p.hidden = true);
    newTab.setAttribute('aria-selected', 'true');
    const panel = document.getElementById(newTab.getAttribute('aria-controls'));
    if (panel) panel.hidden = false;
  }

  tabs.forEach(tab => {
    tab.addEventListener('click', () => activateTab(tab));
    tab.addEventListener('keydown', e => {
      const idx = [...tabs].indexOf(e.target);
      if (e.key === 'ArrowRight' && idx < tabs.length - 1) {
        e.preventDefault();
        activateTab(tabs[idx + 1]);
        tabs[idx + 1].focus();
      } else if (e.key === 'ArrowLeft' && idx > 0) {
        e.preventDefault();
        activateTab(tabs[idx - 1]);
        tabs[idx - 1].focus();
      }
    });
  });
})();
</script>

Keyboard behavior: Left and Right arrow keys switch between tabs. The Tab key moves focus into the active tab panel. Enter and Space activate the focused tab.

Accessible Accordion

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

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

<script>
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;
  });
});
</script>

Accessible Modal Dialog

<button onclick="openDialog()">Open Feedback Form</button>

<div role="dialog" aria-modal="true" aria-labelledby="dialog-title"
     id="feedback-dialog" hidden>
  <div role="document">
    <h2 id="dialog-title">Send Feedback</h2>
    <label for="feedback-text">Your message</label>
    <textarea id="feedback-text" rows="4"></textarea>
    <button onclick="submitFeedback()">Send</button>
    <button onclick="closeDialog()" aria-label="Close dialog">×</button>
  </div>
</div>

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

<script>
function openDialog() {
  const d = document.getElementById('feedback-dialog');
  const b = document.getElementById('dialog-backdrop');
  d.hidden = false;
  b.hidden = false;
  d.querySelector('button, input, textarea').focus();
}

function closeDialog() {
  document.getElementById('feedback-dialog').hidden = true;
  document.getElementById('dialog-backdrop').hidden = true;
  document.querySelector('[onclick="openDialog()"]').focus();
}
</script>

Automated ARIA Validation

// aria-validator.js — validate ARIA usage on any page
async function validateARIAOnPage() {
  const violations = [];
  const elements = document.querySelectorAll('[role], [aria-*]');

  elements.forEach(el => {
    // Check for redundant ARIA on native elements
    const tag = el.tagName.toLowerCase();
    const role = el.getAttribute('role');
    const redundantRoles = {
      'nav': 'navigation', 'main': 'main', 'header': 'banner',
      'footer': 'contentinfo', 'aside': 'complementary', 'form': 'form',
      'button': 'button', 'a': 'link'
    };
    if (redundantRoles[tag] === role) {
      violations.push({
        element: `<${tag}>`,
        issue: `Redundant role="${role}" on native <${tag}> element`,
        severity: 'minor'
      });
    }

    // Check aria-expanded has matching aria-controls
    if (el.hasAttribute('aria-expanded') && !el.hasAttribute('aria-controls')) {
      violations.push({
        element: `<${tag}>`,
        issue: 'aria-expanded without aria-controls',
        severity: 'serious'
      });
    }
  });

  console.table(violations);
  return violations;
}

// Usage
validateARIAOnPage();

Expected output: A table showing every ARIA element with any detected issues — redundant roles, missing pairings, and other common mistakes.

Common ARIA Mistakes

1. Redundant ARIA on Native Elements

Adding role="navigation" to <nav>, role="button" to <button>, or role="heading" to <h1> is unnecessary and can confuse older assistive technologies.

2. Overusing role="alert"

Every time you show a notification, adding role="alert" interrupts the user's current task. Reserve it for time-critical errors. Use aria-live="polite" for routine updates.

3. Forgetting to Update ARIA States

Setting aria-expanded="false" in your HTML but never updating it via JavaScript means screen readers always report "collapsed" even when the content is visible.

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

An element with aria-hidden="true" that contains focusable children creates a keyboard trap. If you must hide it, also set tabindex="-1" on children or move them out.

5. Applying role="presentation" to Focusable Elements

If a focusable element has role="presentation", screen readers will not announce it, but keyboard users can still focus on it — a confusing dead end.

6. Not Providing an Accessible Name

Widgets like role="tab", role="dialog", and role="progressbar" must have an accessible name via aria-label, aria-labelledby, or visible text. Without one, screen readers cannot identify them.

7. Using Too Many Live Regions

Each aria-live region adds overhead. A page with 10+ live regions overwhelms screen reader users. Consolidate updates into a single region where possible.

Practice Questions

1. What is the first rule of ARIA? Use a native HTML element before resorting to ARIA. Native elements have built-in semantics, keyboard support, and focus management that ARIA only approximates.

2. What is the difference between aria-label and aria-labelledby? aria-label provides the accessible name as a string value. aria-labelledby references the ID of another element whose text content becomes the accessible name. aria-labelledby takes precedence over aria-label.

3. When should you use role="alert" versus aria-live="polite"? Use role="alert" for time-sensitive, critical messages that must interrupt the user immediately. Use aria-live="polite" for routine updates that can wait until the user is idle.

4. What happens if you put aria-hidden="true" on a focusable element? The element becomes invisible to screen readers but remains focusable via keyboard. Users can Tab to it but hear nothing — a confusing experience often called a "focus trap."

5. Challenge: Build an accessible custom star rating widget using role="radiogroup", role="radio", and aria-checked. It should support arrow key navigation, space to select, and announce the selected rating.

Real-World Task

Audit a page you maintain for ARIA usage. Open Chrome DevTools, Go to the Accessibility panel, and inspect the Accessibility tree. Identify all ARIA attributes, check if they are correctly applied, and fix any redundant, missing, or incorrect usage.

FAQ

Can ARIA fix all Accessibility problems? No. ARIA only affects the Accessibility tree — what screen readers perceive. It does not fix keyboard navigation, color contrast, focus management, zoom, or any other non-ARIA Accessibility requirement.

How do screen readers handle unsupported ARIA roles? If a screen reader encounters an ARIA role it does not support, it typically falls back to the element's native semantics. For example, an unsupported role="foo" on a <button> will still be announced as "button," but the custom role is ignored.

Should I always use aria-label on icon buttons? Yes. An icon button with no text (like a magnifying glass for search) needs aria-label="Search" to give screen readers an accessible name. Without it, the button is announced as "button" with no label.

Does ARIA affect keyboard behavior? Not automatically. ARIA roles alone do not add keyboard handlers. If you use role="button" on a <div>, you must also add tabindex="0" and JavaScript for Enter and Space key handling.

Can I use multiple aria-labelledby IDs? Yes. aria-labelledby accepts a space-separated list of IDs. The text content of all referenced elements is concatenated in order to form the accessible name.

Try It Yourself

Build an accessible ARIA validator that scans a page for redundant roles:

// redundant-role-checker.js
(function checkRedundantRoles() {
  const map = {
    'nav': 'navigation', 'main': 'main', 'header': 'banner',
    'footer': 'contentinfo', 'aside': 'complementary', 'button': 'button',
    'a': 'link', 'form': 'form', 'table': 'table'
  };
  const results = [];
  document.querySelectorAll('[role]').forEach(el => {
    const tag = el.tagName.toLowerCase();
    if (map[tag] === el.getAttribute('role')) {
      results.push({ element: `<${tag}>`, role: el.getAttribute('role'), status: 'REDUNDANT' });
    }
  });
  if (results.length === 0) {
    console.log('✅ No redundant ARIA roles found');
  } else {
    console.table(results);
  }
})();

Expected output: A table listing any elements with redundant ARIA roles, or a confirmation message if none are found.

What's Next

Keyboard Navigation Guide
Screen Readers — NVDA, JAWS & VoiceOver Guide
Accessible Forms Guide

Congratulations on completing this ARIA guide! Here is where to Go from here:

  • Practice daily — Add proper ARIA to one component per day
  • Build a project — Create an accessible dialog 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