Difficulty: Intermediate
WAI-ARIA (Web Accessibility Initiative - Accessible Rich Internet Applications) is a specification that defines a set of attributes you can add to HTML elements to communicate additional semantics to assistive technologies. ARIA roles, states, and properties fill the gaps where native HTML semantics fall short, particularly for custom interactive components like tabs, modals, drag-and-drop interfaces, and live-updating regions.
The role attribute defines what an element is or does. ARIA defines several categories of roles. Landmark roles (banner, navigation, main, complementary, contentinfo, form, region, search) mirror the semantic HTML elements - <header> implicitly has role="banner", <nav> has role="navigation", and so on. Widget roles (button, tab, tabpanel, dialog, slider, menuitem, etc.) describe interactive components. Document structure roles (article, heading, list, listitem, img, toolbar, etc.) describe non-interactive structures.
The first rule of ARIA is: do not use ARIA if you can use a native HTML element instead. A <button> is always better than <div role="button"> because the native element comes with built-in keyboard handling, focus management, and form submission. ARIA does not add behavior - it only communicates semantics. If you use role="button" on a div, you must manually add keyboard event handlers (Enter and Space), tabindex="0" for focus, and visual focus indicators. The native <button> gives you all of this for free.
ARIA states and properties convey dynamic information about elements. Key attributes include: aria-label (provides an accessible name when visible text is insufficient), aria-labelledby (points to the id of the element that labels this one), aria-describedby (points to a description), aria-expanded (whether a collapsible section is open), aria-hidden (hides element from screen readers), aria-live (announces dynamic content changes), aria-required, and aria-disabled. These attributes let screen readers announce current states and relationships that are visually apparent but not expressed in the DOM.
ARIA live regions are particularly important for single-page applications. When content updates dynamically (notifications, chat messages, form validation errors), screen readers need to be told about changes. Setting aria-live="polite" on a container means the screen reader will announce changes at the next convenient pause. Setting aria-live="assertive" interrupts whatever the screen reader is currently saying to announce the change immediately. Use assertive sparingly - only for critical alerts and errors.
<!-- Prefer native HTML elements (built-in semantics + behavior) -->
<button>Save</button>
<nav>
<a href="/home">Home</a>
</nav>
<main>
<h1>Page Title</h1>
</main>
<!-- ARIA equivalents (require manual behavior implementation) -->
<div role="button" tabindex="0"
onkeydown="if(event.key==='Enter'||event.key===' ')this.click()">
Save
</div>
<div role="navigation">
<a href="/home">Home</a>
</div>
<div role="main">
<div role="heading" aria-level="1">Page Title</div>
</div>
<!-- The native versions are ALWAYS preferred.
They include keyboard handling, focus, and
form behavior for free. -->
The native HTML elements on top provide the same semantics as the ARIA roles below, plus built-in keyboard handling, focus management, and other behaviors. ARIA only adds semantics - never behavior.
<!-- aria-label: provides accessible name directly -->
<button aria-label="Close dialog">
<svg viewBox="0 0 24 24"><path d="M6 18L18 6M6 6l12 12"/></svg>
</button>
<!-- aria-labelledby: references another element's text -->
<section aria-labelledby="pricing-title">
<h2 id="pricing-title">Pricing Plans</h2>
<p>Choose the plan that fits your needs.</p>
</section>
<!-- aria-describedby: provides additional description -->
<label for="password">Password</label>
<input type="password" id="password"
aria-describedby="password-help">
<p id="password-help">
Must be at least 8 characters with one uppercase letter and one number.
</p>
<!-- Combining labels for complex components -->
<div role="group" aria-labelledby="shipping-label">
<h3 id="shipping-label">Shipping Address</h3>
<label for="street">Street</label>
<input id="street" type="text">
<label for="city">City</label>
<input id="city" type="text">
</div>
aria-label directly sets the accessible name (used for icon buttons). aria-labelledby points to visible text that names the element. aria-describedby provides supplementary information announced after the element's name and role.
<!-- Polite: announced at next pause -->
<div aria-live="polite" id="status">
<!-- Status messages appear here -->
</div>
<!-- Assertive: interrupts immediately -->
<div aria-live="assertive" role="alert" id="error">
<!-- Critical errors appear here -->
</div>
<script>
// When a form is submitted successfully:
document.getElementById('status').textContent =
'Your changes have been saved.';
// Screen reader announces: "Your changes have been saved."
// When a critical error occurs:
document.getElementById('error').textContent =
'Session expired. Please log in again.';
// Screen reader immediately announces the error
</script>
<!-- Common live region patterns -->
<!-- Toast notifications -->
<div aria-live="polite" role="status" class="toast-container">
</div>
<!-- Form validation -->
<div aria-live="polite">
<span class="error">Email address is required.</span>
</div>
Live regions tell screen readers to announce content changes. Use 'polite' for non-urgent updates (save confirmations, status changes). Use 'assertive' only for critical alerts that require immediate attention.
<!-- Expandable section -->
<button aria-expanded="false"
aria-controls="faq-answer-1"
onclick="toggleFaq(this)">
What is WAI-ARIA?
</button>
<div id="faq-answer-1" hidden>
<p>WAI-ARIA is a specification for accessible web applications...</p>
</div>
<!-- Tab interface -->
<div role="tablist" aria-label="Product information">
<button role="tab" aria-selected="true"
aria-controls="panel-desc" id="tab-desc">
Description
</button>
<button role="tab" aria-selected="false"
aria-controls="panel-reviews" id="tab-reviews">
Reviews
</button>
</div>
<div role="tabpanel" id="panel-desc"
aria-labelledby="tab-desc">
<p>This product features...</p>
</div>
<div role="tabpanel" id="panel-reviews"
aria-labelledby="tab-reviews" hidden>
<p>Customer reviews...</p>
</div>
<script>
function toggleFaq(button) {
const expanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', String(!expanded));
const content = document.getElementById(
button.getAttribute('aria-controls')
);
content.hidden = expanded;
}
</script>
aria-expanded communicates whether a collapsible section is open. aria-controls links a button to the element it controls. For tabs, aria-selected marks the active tab, and role='tabpanel' with aria-labelledby creates the association between tab and panel.
WAI-ARIA, role Attribute, Landmark Roles, aria-label, aria-live, Accessible Names