Imagine you're giving directions to someone who can't see the visual landmarks you normally rely on. Instead of saying "turn at the big red building," you'd need to describe the building's purpose: "turn at the fire station" or "turn at the shopping center." ARIA roles work similarly—they describe the purpose and behavior of web page elements to assistive technologies when visual cues aren't available.
ARIA (Accessible Rich Internet Applications) is a set of HTML attributes that provide semantic information about elements to assistive technologies like screen readers. While HTML has built-in semantic elements (like buttons, headings, and forms), ARIA fills the gaps when you create custom components or need to provide additional context that HTML alone cannot express.
ARIA serves several critical functions that make modern web applications accessible to users of assistive technologies:
Without ARIA, many modern web interfaces would be completely incomprehensible to screen reader users, as they'd encounter generic div and span elements with no indication of their purpose or functionality.
As web applications have become more sophisticated and interactive, they've moved beyond the simple document structure that HTML was originally designed for. ARIA provides the vocabulary needed to make these complex, app-like interfaces accessible, ensuring that innovation in user experience doesn't come at the cost of exclusion for users of assistive technologies.
ARIA can be organized into three main categories, each serving different accessibility needs:
Roles define what an element is or does. They're like job descriptions for HTML elements.
<!-- Landmark roles for page structure -->
<div role="banner">Site header content</div>
<div role="navigation">Main menu</div>
<div role="main">Primary page content</div>
<div role="contentinfo">Footer content</div>
<!-- Widget roles for interactive elements -->
<div role="button" tabindex="0">Custom Button</div>
<div role="tab" tabindex="0">Tab Label</div>
<div role="menuitem" tabindex="-1">Menu Option</div>
<!-- Document structure roles -->
<div role="heading" aria-level="2">Section Title</div>
<div role="list">
<div role="listitem">Item 1</div>
<div role="listitem">Item 2</div>
</div>
Properties describe the nature of elements, providing additional information that doesn't change frequently.
<!-- Labels and descriptions -->
<input type="text" aria-label="Search products">
<input type="password" aria-describedby="pwd-help">
<div id="pwd-help">Password must be 8+ characters</div>
<!-- Relationships -->
<div role="tabpanel" aria-labelledby="tab1">
Content for tab 1
</div>
<div role="tab" id="tab1">Settings</div>
<!-- Required fields -->
<input type="email" aria-required="true" placeholder="Email address">
<!-- Read-only or disabled states -->
<input type="text" aria-readonly="true" value="Cannot edit this">
<button aria-disabled="true">Not available</button>
States describe current conditions of elements that change based on user interaction.
<!-- Expanded/collapsed states -->
<button aria-expanded="false" aria-controls="menu">Menu</button>
<ul id="menu" hidden>
<li><a href="/home">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
<!-- Checked states -->
<div role="checkbox" aria-checked="false" tabindex="0">
Subscribe to newsletter
</div>
<!-- Pressed states -->
<button aria-pressed="false">Toggle Bold</button>
<!-- Hidden states -->
<div aria-hidden="true">Decorative element not relevant to screen readers</div>
<!-- Live regions for dynamic content -->
<div aria-live="polite" id="status"></div>
<div aria-live="assertive" id="error-messages"></div>
Understanding the most commonly needed ARIA roles helps you choose the right approach for different situations:
Landmark roles help users understand and navigate page structure, similar to HTML5 semantic elements.
<!-- Use when HTML5 semantic elements aren't suitable -->
<div role="banner">
<!-- Site header, logo, top navigation -->
</div>
<div role="navigation" aria-label="Main menu">
<!-- Primary site navigation -->
</div>
<div role="main">
<!-- Primary page content -->
</div>
<div role="search">
<!-- Search functionality -->
</div>
<div role="complementary">
<!-- Sidebar, related content -->
</div>
<div role="contentinfo">
<!-- Footer, copyright, links -->
</div>
<!-- Note: HTML5 equivalents are preferred when possible -->
<!-- <header>, <nav>, <main>, <aside>, <footer> -->
Widget roles define custom interactive elements that don't have HTML equivalents.
<!-- Button role for non-button elements -->
<div role="button" tabindex="0" onclick="performAction()">
Custom Action
</div>
<!-- Tab interface -->
<div role="tablist">
<div role="tab" aria-selected="true" tabindex="0">Tab 1</div>
<div role="tab" aria-selected="false" tabindex="-1">Tab 2</div>
</div>
<div role="tabpanel" aria-labelledby="tab1">
Content for tab 1
</div>
<!-- Custom checkbox -->
<div role="checkbox"
aria-checked="false"
tabindex="0"
onclick="toggleCheck()">
Custom checkbox option
</div>
<!-- Menu system -->
<div role="menubar">
<div role="menuitem" tabindex="0">File</div>
<div role="menuitem" tabindex="-1">Edit</div>
</div>
These roles help organize content when HTML elements don't provide sufficient semantic meaning.
<!-- Custom headings when h1-h6 aren't appropriate -->
<div role="heading" aria-level="3">
Section Subtitle
</div>
<!-- Lists created with divs instead of ul/ol -->
<div role="list">
<div role="listitem">First item</div>
<div role="listitem">Second item</div>
<div role="listitem">Third item</div>
</div>
<!-- Article-like content -->
<div role="article">
<div role="heading" aria-level="2">Article Title</div>
<p>Article content...</p>
</div>
<!-- Group related form fields -->
<div role="group" aria-labelledby="billing-heading">
<h3 id="billing-heading">Billing Information</h3>
<!-- form fields -->
</div>
Beyond roles, several ARIA attributes are essential for creating accessible interfaces:
These attributes provide accessible names for elements.
<!-- aria-label: Provides accessible name directly -->
<button aria-label="Close dialog">×</button>
<input type="search" aria-label="Search products">
<!-- aria-labelledby: References other element(s) for the name -->
<h2 id="settings-title">Account Settings</h2>
<div role="tabpanel" aria-labelledby="settings-title">
Settings content...
</div>
<!-- aria-describedby: References additional descriptive text -->
<input type="password"
aria-describedby="pwd-requirements"
placeholder="Password">
<div id="pwd-requirements">
Must be 8+ characters with letters and numbers
</div>
These communicate the current state of interactive elements.
<!-- aria-expanded: For collapsible content -->
<button aria-expanded="false" aria-controls="dropdown-menu">
Options
</button>
<ul id="dropdown-menu" hidden>
<li>Option 1</li>
<li>Option 2</li>
</ul>
<!-- aria-checked: For checkboxes and radio buttons -->
<div role="checkbox" aria-checked="true" tabindex="0">
Notifications enabled
</div>
<!-- aria-pressed: For toggle buttons -->
<button aria-pressed="false" onclick="toggleBold()">
Bold
</button>
<!-- aria-disabled: For temporarily disabled elements -->
<button aria-disabled="true">
Save (form incomplete)
</button>
These announce dynamic content changes to screen readers.
<!-- aria-live="polite": Announces when user is idle -->
<div id="status" aria-live="polite">
<!-- Status messages appear here -->
</div>
<!-- aria-live="assertive": Announces immediately -->
<div id="errors" aria-live="assertive">
<!-- Error messages appear here -->
</div>
<!-- Common pattern for form validation -->
<input type="email" aria-describedby="email-error">
<div id="email-error" aria-live="polite" role="alert">
<!-- Error message appears here when validation fails -->
</div>
<script>
// Example of updating live regions
function showStatus(message) {
document.getElementById('status').textContent = message;
}
function showError(message) {
document.getElementById('errors').textContent = message;
}
</script>
These describe how elements relate to each other.
<!-- aria-controls: Element controls another element -->
<button aria-controls="sidebar" onclick="toggleSidebar()">
Toggle Sidebar
</button>
<div id="sidebar">Sidebar content</div>
<!-- aria-owns: Establishes parent-child relationship -->
<div role="listbox" aria-owns="option1 option2 option3">
<div role="option" id="option1">Option 1</div>
<div role="option" id="option2">Option 2</div>
<div role="option" id="option3">Option 3</div>
</div>
<!-- aria-flowto: Indicates reading order -->
<div id="step1" aria-flowto="step2">
Step 1: Enter your email
</div>
<div id="step2" aria-flowto="step3">
Step 2: Verify your email
</div>
<div id="step3">
Step 3: Complete setup
</div>
ARIA is powerful but can cause problems when used incorrectly. Here are the most common mistakes:
What's happening: Adding ARIA roles to generic elements when semantic HTML elements would work better.
Simple solution: Always prefer semantic HTML when available. ARIA should enhance, not replace, good HTML:
<!-- Bad: Using ARIA where HTML would work -->
<div role="button" tabindex="0" onclick="submit()">Submit</div>
<div role="heading" aria-level="1">Page Title</div>
<!-- Good: Use semantic HTML first -->
<button type="submit" onclick="submit()">Submit</button>
<h1>Page Title</h1>
<!-- ARIA is appropriate when HTML isn't sufficient -->
<div role="button" tabindex="0" onclick="customAction()" class="special-styling">
Custom Styled Action
</div>
What's happening: Adding ARIA roles that conflict with the element's implicit role or changing roles inappropriately.
Simple solution: Don't override implicit semantics unless absolutely necessary:
<!-- Bad: Conflicting semantics -->
<button role="link">This is confusing</button>
<a href="#" role="button">This is also confusing</a>
<!-- Good: Consistent semantics -->
<button onclick="performAction()">Action Button</button>
<a href="/page">Link to Page</a>
<!-- If you must change semantics, ensure it makes sense -->
<a href="/download" role="button" download>
Download File
</a> <!-- This could be appropriate for styling consistency -->
What's happening: ARIA states are set initially but not updated when the interface changes.
Simple solution: Always update ARIA states when the corresponding UI state changes:
// Bad: Only setting initial state
<button aria-expanded="false" onclick="toggleMenu()">Menu</button>
// Good: Updating state when UI changes
<button aria-expanded="false" onclick="toggleMenu()" id="menu-button">Menu</button>
<ul id="menu" hidden>
<li>Option 1</li>
<li>Option 2</li>
</ul>
<script>
function toggleMenu() {
const button = document.getElementById('menu-button');
const menu = document.getElementById('menu');
const isExpanded = button.getAttribute('aria-expanded') === 'true';
// Update both visual state and ARIA state
button.setAttribute('aria-expanded', !isExpanded);
menu.hidden = isExpanded;
}
</script>
What's happening: Using aria-hidden="true" on content that should be accessible to screen readers.
Simple solution: Only use aria-hidden for purely decorative content or when you're providing alternative accessible content:
<!-- Good uses of aria-hidden -->
<button>
<span aria-hidden="true">🔍</span> <!-- Decorative icon -->
Search
</button>
<div class="decorative-divider" aria-hidden="true"></div>
<!-- Bad: Hiding important content -->
<div aria-hidden="true">
Important instructions for users
</div> <!-- Screen readers won't access this -->
<!-- Better: Let screen readers access important content -->
<div>
Important instructions for users
</div>
What's happening: Adding ARIA roles without implementing the expected keyboard interactions.
Simple solution: When you use ARIA widget roles, implement the keyboard patterns users expect:
<!-- Bad: ARIA role without keyboard support -->
<div role="button" onclick="performAction()">Custom Button</div>
<!-- Good: ARIA role with proper keyboard support -->
<div role="button"
tabindex="0"
onclick="performAction()"
onkeydown="handleKeyPress(event)">
Custom Button
</div>
<script>
function handleKeyPress(event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
performAction();
}
}
</script>
Some complex interfaces require sophisticated ARIA implementation:
Proper ARIA implementation for modal dialogs requires multiple attributes working together.
<!-- Modal trigger -->
<button onclick="openModal()" id="open-modal">Open Settings</button>
<!-- Modal dialog -->
<div role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
id="settings-modal"
hidden>
<h2 id="modal-title">Settings</h2>
<p id="modal-description">Configure your account preferences</p>
<!-- Modal content -->
<form>
<label for="notifications">Email notifications</label>
<input type="checkbox" id="notifications">
<button type="submit">Save</button>
<button type="button" onclick="closeModal()">Cancel</button>
</form>
<button onclick="closeModal()" aria-label="Close settings dialog">×</button>
</div>
<script>
function openModal() {
const modal = document.getElementById('settings-modal');
modal.hidden = false;
modal.querySelector('input').focus(); // Focus first input
document.body.setAttribute('aria-hidden', 'true'); // Hide background content
}
function closeModal() {
const modal = document.getElementById('settings-modal');
modal.hidden = true;
document.body.removeAttribute('aria-hidden');
document.getElementById('open-modal').focus(); // Return focus
}
</script>
Complex tables benefit from ARIA attributes that describe their structure and functionality.
<table role="table" aria-label="Product inventory">
<thead>
<tr role="row">
<th role="columnheader"
aria-sort="ascending"
tabindex="0"
onclick="sortTable('name')">
Product Name
</th>
<th role="columnheader"
aria-sort="none"
tabindex="0"
onclick="sortTable('price')">
Price
</th>
<th role="columnheader"
aria-sort="none"
tabindex="0"
onclick="sortTable('stock')">
Stock
</th>
</tr>
</thead>
<tbody>
<tr role="row">
<td role="cell">Widget A</td>
<td role="cell">$19.99</td>
<td role="cell">150</td>
</tr>
<!-- More rows... -->
</tbody>
</table>
<script>
function sortTable(column) {
// Sorting logic here...
// Update aria-sort attributes
document.querySelectorAll('[aria-sort]').forEach(th => {
th.setAttribute('aria-sort', 'none');
});
// Set the sorted column
event.target.setAttribute('aria-sort', 'ascending'); // or 'descending'
}
</script>
Thorough testing ensures your ARIA implementation works as intended:
The most important test is whether your implementation matches user expectations—does the screen reader output accurately represent what's happening visually on screen?
Correct ARIA implementation delivers significant business benefits:
These benefits combine to create more inclusive, usable, and successful web applications that serve all users effectively.
Different types of web applications benefit from specific ARIA implementation strategies:
In each case, the key is understanding how users with different abilities will interact with your interface and using ARIA to bridge gaps where standard HTML falls short.
ARIA represents one of the most powerful tools in the accessibility toolkit, but also one that requires careful, thoughtful implementation. When used correctly, it transforms custom web components from mysterious, inaccessible elements into clear, usable interfaces that work for everyone.
The key to successful ARIA implementation lies in understanding that it's not just about adding attributes—it's about creating experiences that make sense to users who interact with your interface in different ways. Every ARIA role, property, and state should serve a clear purpose in communicating what elements do and how they behave.
What makes ARIA particularly valuable in modern web development is its role in enabling innovation without exclusion. As user interfaces become more sophisticated and app-like, ARIA ensures that these advances in user experience don't come at the cost of accessibility.
Remember that ARIA is most effective when it enhances semantic HTML rather than replacing it. The best accessible interfaces combine solid HTML foundations with thoughtful ARIA enhancement, creating experiences that are both innovative and inclusive.
Greadme's easy-to-use tools can help you identify missing or incorrect ARIA implementation on your website and provide guidance on proper usage—even if you're not technically minded.
Analyze Your Website's ARIA Implementation Today