ARIA Roles and Attributes
Complete guide to implementing accessibility with ARIA roles and attributes
Empowering Accessibility with ARIA: Roles & Attributes Made Easy
ARIA (Accessible Rich Internet Applications) helps bridge accessibility gaps in custom UI components. It enables assistive technologies like screen readers to interpret complex interactions that native HTML can't always cover.
Here's your friendly deep dive into ARIA roles and attributes—what they do, why they matter, and how to use them like a pro
Common ARIA Roles
ARIA roles describe the purpose of an element to assistive technologies.
| Role | What It Means | Example Use Case |
|---|---|---|
role="button" | Acts like a clickable button | Custom button components |
role="link" | Functions as a hyperlink | JavaScript navigation |
role="heading" | Defines a heading level, with aria-level for depth | Dynamic headings |
role="navigation" | Denotes a section of navigational content | Custom navigation menus |
role="checkbox" | Represents a toggleable checkbox input | Custom checkbox components |
role="menu" / role="menuitem" | Describes a menu and its options | Dropdown menus |
role="alert" | Announces urgent info like errors or warnings | Error messages |
role="dialog" | Represents a dialog or modal window | Modal components |
role="tab" / role="tabpanel" | Defines tab interface components | Tabbed interfaces |
role="progressbar" | Shows progress of a task | Loading indicators |
Practical Examples
<!-- Custom Button -->
<div
role="button"
tabindex="0"
aria-pressed="false"
onkeydown="handleKeyDown(event)"
onclick="handleClick()"
>
Toggle Theme
</div>
<!-- Custom Link -->
<div
role="link"
tabindex="0"
aria-label="Visit our homepage"
onkeydown="handleKeyDown(event)"
onclick="navigateToHome()"
>
<span class="icon">🏠</span>
</div>
<!-- Dynamic Heading -->
<div role="heading" aria-level="2">Section Title</div>
<!-- Navigation Menu -->
<nav role="navigation" aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
<!-- Custom Checkbox -->
<div
role="checkbox"
aria-checked="true"
tabindex="0"
aria-labelledby="checkbox-label"
onclick="toggleCheckbox()"
>
<span id="checkbox-label">Subscribe to newsletter</span>
</div>
<!-- Dropdown Menu -->
<div role="menu" aria-label="User options">
<div role="menuitem" tabindex="0" onclick="editProfile()">Edit Profile</div>
<div role="menuitem" tabindex="0" onclick="settings()">Settings</div>
<div role="menuitem" tabindex="0" onclick="logout()">Logout</div>
</div>
<!-- Alert Message -->
<div role="alert" aria-live="assertive">
Your changes have been saved successfully!
</div>
<!-- Modal Dialog -->
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Confirm Action</h2>
<p id="dialog-description">Are you sure you want to delete this item?</p>
<button onclick="confirm()">Yes</button>
<button onclick="cancel()">No</button>
</div>
<!-- Tab Interface -->
<div role="tablist" aria-label="Product information">
<button role="tab" aria-selected="true" aria-controls="panel1">Description</button>
<button role="tab" aria-selected="false" aria-controls="panel2">Specifications</button>
<button role="tab" aria-selected="false" aria-controls="panel3">Reviews</button>
</div>
<div role="tabpanel" id="panel1" aria-labelledby="tab1">Product description...</div>
<div role="tabpanel" id="panel2" aria-labelledby="tab2" hidden>Product specs...</div>
<div role="tabpanel" id="panel3" aria-labelledby="tab3" hidden>Product reviews...</div>
<!-- Progress Bar -->
<div
role="progressbar"
aria-valuenow="75"
aria-valuemin="0"
aria-valuemax="100"
aria-label="Upload progress"
>
<div class="progress-fill" style="width: 75%"></div>
</div>Key ARIA Attributes
ARIA attributes describe an element's state, name, and behavior, often in real time.
| Attribute | Purpose | Example |
|---|---|---|
aria-label | Gives an element a name read by screen readers | <button aria-label="Close dialog">✖</button> |
aria-labelledby | References another element's ID as the label | <section aria-labelledby="section-title"> |
aria-describedby | Provides extra context by referencing another element | <input aria-describedby="username-help"> |
aria-hidden | Hides an element from assistive tech without removing it | <div aria-hidden="true">Decorative graphic</div> |
aria-disabled | Indicates the element is disabled (visually + functionally) | <button aria-disabled="true">Submit</button> |
aria-haspopup | Shows that a control triggers a popup (like a menu) | <button aria-haspopup="true">Open Menu</button> |
aria-expanded | Tells whether a collapsible element is open | <button aria-expanded="false">Show Details</button> |
aria-live | Marks a region as dynamically updated (e.g., notifications) | <div aria-live="polite">New message received</div> |
aria-pressed | Describes the toggled state of buttons | <button aria-pressed="true">Pause</button> |
aria-current | Indicates the current item in a set | <a aria-current="page">Current Page</a> |
Comprehensive Examples
<!-- Form with proper labeling -->
<form>
<label for="username">Username:</label>
<input
type="text"
id="username"
aria-describedby="username-help username-error"
aria-required="true"
/>
<div id="username-help">Enter your full username</div>
<div id="username-error" role="alert" aria-live="polite"></div>
</form>
<!-- Section with proper labeling -->
<section aria-labelledby="section-title">
<h2 id="section-title">Billing Information</h2>
<p>Please provide your billing details below.</p>
</section>
<!-- Interactive component with state -->
<div class="accordion">
<button
aria-expanded="false"
aria-controls="panel1"
onclick="togglePanel('panel1')"
>
Section 1
</button>
<div id="panel1" aria-labelledby="panel1-button" hidden>
Content for section 1...
</div>
</div>
<!-- Live region for updates -->
<div aria-live="polite" aria-atomic="true">
<span id="notification">Ready</span>
</div>
<!-- Navigation with current page -->
<nav aria-label="Main navigation">
<ul>
<li><a href="/" aria-current="page">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>Advanced ARIA Patterns
Custom Select Component
<div class="custom-select" role="combobox" aria-expanded="false" aria-haspopup="listbox">
<button
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-labelledby="select-label"
onclick="toggleSelect()"
>
<span id="select-label">Choose an option</span>
<span aria-hidden="true">▼</span>
</button>
<ul role="listbox" aria-label="Options" hidden>
<li role="option" aria-selected="false" onclick="selectOption('option1')">Option 1</li>
<li role="option" aria-selected="false" onclick="selectOption('option2')">Option 2</li>
<li role="option" aria-selected="false" onclick="selectOption('option3')">Option 3</li>
</ul>
</div>Search Results with Live Updates
<div role="search">
<label for="search-input">Search:</label>
<input
type="search"
id="search-input"
aria-describedby="search-results"
oninput="performSearch(this.value)"
/>
<div
id="search-results"
role="status"
aria-live="polite"
aria-atomic="true"
>
Type to search...
</div>
</div>Error Handling
<form onsubmit="handleSubmit(event)">
<div class="form-group">
<label for="email">Email:</label>
<input
type="email"
id="email"
aria-describedby="email-error"
aria-invalid="false"
onblur="validateEmail(this)"
/>
<div
id="email-error"
role="alert"
aria-live="polite"
class="error-message"
></div>
</div>
</form>JavaScript for ARIA
// Toggle accordion panel
function togglePanel(panelId) {
const button = document.querySelector(`[aria-controls="${panelId}"]`);
const panel = document.getElementById(panelId);
const isExpanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', !isExpanded);
panel.hidden = isExpanded;
}
// Update live regions
function updateNotification(message) {
const notification = document.getElementById('notification');
notification.textContent = message;
}
// Handle keyboard navigation
function handleKeyDown(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
event.target.click();
}
}
// Validate form with ARIA
function validateEmail(input) {
const errorElement = document.getElementById('email-error');
const email = input.value;
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
input.setAttribute('aria-invalid', !isValid);
if (!isValid) {
errorElement.textContent = 'Please enter a valid email address';
} else {
errorElement.textContent = '';
}
}Best Practices
Do's
- Use semantic HTML first, then layer ARIA only when needed
- Test with screen readers and accessibility tools
- Keep ARIA attributes up to date with dynamic content
- Use
aria-livesparingly to avoid overwhelming users - Provide fallbacks for complex interactions
Don'ts
- Don't overuse ARIA when semantic HTML works
- Don't rely solely on ARIA for functionality
- Don't forget keyboard navigation
- Don't use ARIA to fix bad HTML structure
Testing Tools
| Tool | What It Does |
|---|---|
| Screen Readers | NVDA (Windows), VoiceOver (Mac), JAWS (Windows) |
| Browser Extensions | axe DevTools, Lighthouse, WAVE |
| Manual Testing | Keyboard-only navigation, color contrast checkers |
| Automated Testing | Jest-axe, Cypress-axe, Playwright accessibility |
Final Tips
- Use semantic HTML first, then layer ARIA only when needed
- Always test with screen readers and accessibility tools like Axe, Lighthouse, or VoiceOver
- Don't forget
tabindex="0"for focusable custom elements - Keep it simple - complex ARIA can be harder to maintain than semantic HTML
With thoughtful use of ARIA roles and attributes, we can create digital experiences that are more inclusive, intuitive, and empowering for everyone
Next: Web Accessibility Best Practices - Learn comprehensive accessibility guidelines for your applications.