ARIA Roles, States, and Properties
1. Introduction
ARIA — Accessible Rich Internet Applications — is a W3C specification that supplements native HTML semantics, particularly for custom interactive components where no suitable native element exists. ARIA attributes fall into three categories: roles (what an element is), states (its current condition), and properties (its characteristics). Used correctly, ARIA bridges the gap between visual UI and the accessibility tree. Used incorrectly, it actively harms assistive technology users.
2. Concept
ARIA Attribute Categories
| Category | Examples | Changes With Interaction? |
|---|---|---|
| Roles | role=”dialog”, role=”alert”, role=”tab”, role=”button” | No — set once |
| States | aria-expanded, aria-checked, aria-selected, aria-disabled | Yes — updated by JavaScript |
| Properties | aria-label, aria-labelledby, aria-describedby, aria-controls | Rarely — mostly static |
<button> is better than <div role="button" tabindex="0"> in almost every case — it requires less code and works without JavaScript.aria-label overrides all other naming sources. aria-labelledby references another element’s text as the accessible name. Prefer aria-labelledby when a visible label already exists — it creates a single source of truth for sighted and non-sighted users.role="tab" must respond to arrow keys. A role="dialog" must trap focus. Failing to do so creates a broken experience that is worse than no ARIA at all.3. Basic Example
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>ARIA Demo</title></head>
<body>
<!-- aria-label: accessible name for icon-only button -->
<button type="button" aria-label="Close dialog">
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 16 16">
<line x1="2" y1="2" x2="14" y2="14" stroke="currentColor" stroke-width="2"/>
<line x1="14" y1="2" x2="2" y2="14" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<!-- aria-expanded: toggle button state -->
<button type="button" aria-expanded="false" aria-controls="panel-1">
Show Details
</button>
<div id="panel-1" hidden>
<p>Expanded panel content.</p>
</div>
<!-- aria-live: announce dynamic content changes -->
<div role="status" aria-live="polite" id="cart-count">
3 items in cart
</div>
<!-- aria-describedby: link hint text to input -->
<label for="password">Password</label>
<input type="password" id="password" name="password" aria-describedby="pw-hint">
<p id="pw-hint">Minimum 8 characters, must include a number.</p>
<!-- aria-labelledby: use visible heading as accessible name -->
<section aria-labelledby="skills-h">
<h2 id="skills-h">Technical Skills</h2>
<p>HTML, CSS, JavaScript, React</p>
</section>
</body>
</html>
4. How It Works
Step 1 — aria-label for Icon Buttons
An icon-only button has no visible text. Without an accessible name, a screen reader announces “button” with no indication of purpose. aria-label="Close dialog" provides the accessible name. aria-hidden="true" on the SVG prevents its path data or title from being announced separately.
Step 2 — aria-expanded and aria-controls
When a button toggles a panel, aria-expanded must reflect current state: "false" when collapsed, "true" when open. aria-controls identifies the controlled element, helping AT users understand the relationship. Always update both via JavaScript on each click.
Step 3 — aria-live Regions
Content injected into the DOM after page load is not announced by screen readers by default. A aria-live="polite" region queues announcements for when the user is idle. Use aria-live="assertive" only for critical errors — it interrupts the current announcement immediately.
Step 4 — aria-describedby vs aria-labelledby
aria-labelledby sets the primary accessible name from another element. aria-describedby adds supplementary description announced after the name and role. Both accept space-separated lists of ids to reference multiple elements in order.
5. Real-World Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessible Accordion FAQ</title>
<style>
.panel[hidden] { display: none; }
button[aria-expanded="true"] .icon { transform: rotate(180deg); }
.icon { display: inline-block; transition: transform 0.2s; }
:focus-visible { outline: 3px solid #0070f3; outline-offset: 2px; }
</style>
</head>
<body>
<h1>Frequently Asked Questions</h1>
<h2>
<button type="button" aria-expanded="false" aria-controls="faq-1" id="faq-btn-1">
What is your returns policy?
<span class="icon" aria-hidden="true">▼</span>
</button>
</h2>
<div id="faq-1" class="panel" role="region" aria-labelledby="faq-btn-1" hidden>
<p>You can return any item within 30 days for a full refund, provided it is unused and in original packaging.</p>
</div>
<h2>
<button type="button" aria-expanded="false" aria-controls="faq-2" id="faq-btn-2">
How long does delivery take?
<span class="icon" aria-hidden="true">▼</span>
</button>
</h2>
<div id="faq-2" class="panel" role="region" aria-labelledby="faq-btn-2" hidden>
<p>Standard delivery: 3–5 business days. Express next-day delivery available for orders before 2 PM.</p>
</div>
<script>
document.querySelectorAll('[aria-controls]').forEach(function(btn) {
btn.addEventListener('click', function() {
var open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
document.getElementById(btn.getAttribute('aria-controls')).hidden = open;
});
});
</script>
</body>
</html>
6. Common Mistakes
❌ ARIA role without keyboard interaction
<div role="button" onclick="doAction()">Click Me</div>
✓ Use native <button> — keyboard support built in
<button type="button" onclick="doAction()">Click Me</button>
❌ Stale aria-expanded not updated after toggle
<button aria-expanded="false" onclick="panel.hidden = false">Show</button>
✓ Always sync aria-expanded with actual panel state
<button aria-expanded="false" id="tog">Show</button>
<script>
var t = document.getElementById('tog');
t.addEventListener('click', function() {
t.setAttribute('aria-expanded', t.getAttribute('aria-expanded') === 'false' ? 'true' : 'false');
});
</script>
7. Try It Yourself
8. Quick Reference
| Attribute | Type | Common Use |
|---|---|---|
aria-label |
Property | Name icon buttons, inputs without visible labels |
aria-labelledby |
Property | Link visible heading as accessible name |
aria-describedby |
Property | Attach hint or error text to inputs |
aria-expanded |
State | Toggle open/closed of controlled element |
aria-controls |
Property | Identify element controlled by a button |
aria-live |
Property | Announce dynamic DOM changes (polite/assertive) |
aria-hidden |
State | Hide decorative elements from assistive technology |