ARIA — Accessible Rich Internet Applications Complete Guide
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>© 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
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
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