ARIA (Accessible Rich Internet Applications) is a set of attributes that make web content and applications more accessible to people with disabilities. ARIA fills gaps in HTML by providing additional information to assistive technologies like screen readers.
What is ARIA?
ARIA is a W3C specification that defines ways to make web content more accessible when native HTML elements aren't sufficient. It's particularly useful for:
- Dynamic content that updates without page reload
- Advanced user interface controls (tabs, sliders, dialogs)
- Custom widgets built with div and span elements
- Providing additional context for assistive technologies
The First Rule of ARIA
No ARIA is better than bad ARIA!
If you can use a native HTML element with the semantics and behavior you need already built in, instead of re-purposing an element and adding ARIA, then do so. Don't use ARIA unless you have to.
<!-- GOOD: Use native HTML button -->
<button>Click me</button>
<!-- BAD: Don't reinvent the wheel with ARIA -->
<div role="button" tabindex="0"
onclick="handleClick()"
onkeypress="handleKeyPress()">
Click me
</div>
<!-- GOOD: Use native checkbox -->
<input type="checkbox" id="agree">
<label for="agree">I agree</label>
<!-- BAD: Custom checkbox requiring ARIA -->
<div role="checkbox" aria-checked="false" tabindex="0">
I agree
</div>Three Components of ARIA
ARIA consists of three main components:
1. Roles
Roles define what an element is or does. They tell assistive technologies the type of element they're interacting with.
<!-- Navigation landmark -->
<div role="navigation">
<a href="/">Home</a>
<a href="/about">About</a>
</div>
<!-- Alert message -->
<div role="alert">
Your session is about to expire!
</div>
<!-- Tab interface -->
<div role="tablist">
<button role="tab" aria-selected="true">Tab 1</button>
<button role="tab" aria-selected="false">Tab 2</button>
</div>
<!-- Dialog/Modal -->
<div role="dialog" aria-labelledby="dialog-title">
<h2 id="dialog-title">Confirm Action</h2>
<p>Are you sure?</p>
</div>2. Properties
Properties define characteristics of elements. They often describe relationships or provide additional information.
<!-- Label an element -->
<button aria-label="Close dialog">×</button>
<!-- Reference another element for label -->
<div id="email-label">Email Address</div>
<input type="email" aria-labelledby="email-label">
<!-- Provide additional description -->
<input type="password"
aria-describedby="password-requirements">
<div id="password-requirements">
Must be at least 8 characters
</div>
<!-- Indicate required field -->
<input type="text" aria-required="true">
<!-- Define element that controls another -->
<button aria-controls="menu-list" aria-expanded="false">
Menu
</button>
<ul id="menu-list" hidden>
<li>Option 1</li>
<li>Option 2</li>
</ul>3. States
States define the current condition of elements. Unlike properties, states change frequently (often with JavaScript).
<!-- Expanded/collapsed state -->
<button aria-expanded="false" aria-controls="accordion-panel">
Show Details
</button>
<div id="accordion-panel" hidden>
Details content...
</div>
<!-- Selected state -->
<div role="tab" aria-selected="true">Active Tab</div>
<div role="tab" aria-selected="false">Inactive Tab</div>
<!-- Checked state -->
<div role="checkbox" aria-checked="true">
Subscribe to newsletter
</div>
<!-- Disabled state -->
<button aria-disabled="true">Submit</button>
<!-- Hidden state -->
<div aria-hidden="true">
This content is hidden from screen readers
</div>
<!-- Busy/loading state -->
<div aria-busy="true">Loading content...</div>Common ARIA Attributes
aria-label
Provides an accessible name when visible text isn't available or sufficient:
<!-- Icon button without visible text -->
<button aria-label="Search">
<svg><!-- search icon --></svg>
</button>
<!-- Link with icon only -->
<a href="https://twitter.com" aria-label="Follow us on Twitter">
<svg><!-- twitter icon --></svg>
</a>
<!-- Close button with X symbol -->
<button aria-label="Close notification">×</button>aria-labelledby
References the ID of another element that labels this element:
<!-- Dialog labeled by its heading -->
<div role="dialog" aria-labelledby="dialog-heading">
<h2 id="dialog-heading">Delete Account</h2>
<p>This action cannot be undone.</p>
<button>Delete</button>
<button>Cancel</button>
</div>
<!-- Form section labeled by heading -->
<section aria-labelledby="billing-heading">
<h3 id="billing-heading">Billing Information</h3>
<!-- form fields -->
</section>
<!-- Can reference multiple IDs -->
<button aria-labelledby="icon-label text-label">
<svg id="icon-label" aria-label="Settings"></svg>
<span id="text-label">Preferences</span>
</button>aria-describedby
References additional descriptive information:
<!-- Input with help text -->
<label for="username">Username</label>
<input type="text"
id="username"
aria-describedby="username-help">
<div id="username-help">
Choose a unique username, 3-20 characters
</div>
<!-- Input with error message -->
<label for="email">Email</label>
<input type="email"
id="email"
aria-invalid="true"
aria-describedby="email-error">
<div id="email-error" role="alert">
Please enter a valid email address
</div>
<!-- Can reference multiple descriptions -->
<input type="password"
aria-describedby="password-help password-requirements">
<div id="password-help">Enter a strong password</div>
<ul id="password-requirements">
<li>At least 8 characters</li>
<li>Include numbers and symbols</li>
</ul>aria-hidden
Hides content from assistive technologies:
<!-- Hide decorative icons -->
<button>
<svg aria-hidden="true"><!-- decorative icon --></svg>
Save Document
</button>
<!-- Hide duplicate content -->
<div>
<h2 id="section-title">Contact Us</h2>
<a href="#contact" aria-labelledby="section-title">
<!-- Hide icon, label already provided -->
<svg aria-hidden="true"><!-- icon --></svg>
</a>
</div>
<!-- Warning: Don't hide focusable elements -->
<!-- BAD: This is confusing -->
<button aria-hidden="true">Click me</button>aria-live
Announces dynamic content changes to screen readers:
<!-- Polite announcements (wait for pause) -->
<div aria-live="polite" aria-atomic="true">
Items in cart: 3
</div>
<!-- Assertive announcements (interrupt) -->
<div role="alert" aria-live="assertive">
Error: Connection lost. Attempting to reconnect...
</div>
<!-- Status messages -->
<div role="status" aria-live="polite">
Last saved at 2:45 PM
</div>
<!-- Log messages -->
<div role="log" aria-live="polite" aria-atomic="false">
<p>Message 1</p>
<p>Message 2</p>
<p>Message 3</p>
</div>Practical Examples
Accessible Accordion
<div class="accordion">
<h3>
<button aria-expanded="false"
aria-controls="section1-content"
id="section1-header">
Section 1
</button>
</h3>
<div id="section1-content"
role="region"
aria-labelledby="section1-header"
hidden>
<p>Content for section 1...</p>
</div>
<h3>
<button aria-expanded="false"
aria-controls="section2-content"
id="section2-header">
Section 2
</button>
</h3>
<div id="section2-content"
role="region"
aria-labelledby="section2-header"
hidden>
<p>Content for section 2...</p>
</div>
</div>
<script>
document.querySelectorAll('.accordion button').forEach(button => {
button.addEventListener('click', function() {
const expanded = this.getAttribute('aria-expanded') === 'true';
const content = document.getElementById(this.getAttribute('aria-controls'));
// Toggle state
this.setAttribute('aria-expanded', !expanded);
content.hidden = expanded;
});
});
</script>Accessible Modal Dialog
<button id="openModal">Open Dialog</button>
<div id="myModal"
role="dialog"
aria-modal="true"
aria-labelledby="modalTitle"
aria-describedby="modalDesc"
hidden>
<h2 id="modalTitle">Confirm Delete</h2>
<p id="modalDesc">
Are you sure you want to delete this item?
This action cannot be undone.
</p>
<button id="confirmBtn">Delete</button>
<button id="cancelBtn">Cancel</button>
<button aria-label="Close dialog">×</button>
</div>
<script>
const modal = document.getElementById('myModal');
const openBtn = document.getElementById('openModal');
let lastFocusedElement;
openBtn.addEventListener('click', () => {
lastFocusedElement = document.activeElement;
modal.hidden = false;
// Focus first element in modal
modal.querySelector('button').focus();
// Trap focus within modal
trapFocus(modal);
});
function closeModal() {
modal.hidden = true;
lastFocusedElement.focus();
}
function trapFocus(element) {
const focusableElements = element.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
element.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (!e.shiftKey && document.activeElement === lastElement) {
e.preventDefault();
firstElement.focus();
}
} else if (e.key === 'Escape') {
closeModal();
}
});
}
</script>Landmark Roles
Landmark roles help users navigate to major sections of a page. Most HTML5 elements have implicit roles, but you can add explicit roles when needed:
<!-- Use semantic HTML (preferred) -->
<header><!-- role="banner" implied --></header>
<nav><!-- role="navigation" implied --></nav>
<main><!-- role="main" implied --></main>
<aside><!-- role="complementary" implied --></aside>
<footer><!-- role="contentinfo" implied --></footer>
<!-- Explicit roles when semantic elements can't be used -->
<div role="banner">Site Header</div>
<div role="navigation">Main Menu</div>
<div role="main">Primary Content</div>
<div role="complementary">Sidebar</div>
<div role="contentinfo">Site Footer</div>
<div role="search">
<input type="search" aria-label="Search site">
</div>
<!-- Multiple landmarks of same type need labels -->
<nav aria-label="Main navigation"><!-- main menu --></nav>
<nav aria-label="Footer navigation"><!-- footer links --></nav>Input Validation with ARIA
<div>
<label for="email">Email *</label>
<input type="email"
id="email"
required
aria-required="true"
aria-invalid="false"
aria-describedby="email-error">
<span id="email-error" role="alert" hidden>
Please enter a valid email address
</span>
</div>
<script>
const emailInput = document.getElementById('email');
const emailError = document.getElementById('email-error');
emailInput.addEventListener('blur', function() {
if (!this.validity.valid) {
this.setAttribute('aria-invalid', 'true');
emailError.hidden = false;
} else {
this.setAttribute('aria-invalid', 'false');
emailError.hidden = true;
}
});
</script>ARIA Best Practices
- Use native HTML first: Semantic HTML is always better than ARIA
- Don't change native semantics: Don't do
<h1 role="button"> - All interactive elements must be operable: Ensure keyboard accessibility
- Don't use aria-hidden on focusable elements: This creates confusion
- Keep ARIA up to date: Update states when UI changes
- Test with screen readers: ARIA only works if implemented correctly
- Use aria-label for icon buttons: Provide text alternatives
- Announce dynamic changes: Use aria-live for real-time updates
Common ARIA Mistakes
<!-- ❌ BAD: Redundant role on semantic element -->
<button role="button">Click me</button>
<!-- ✅ GOOD: Use semantic HTML -->
<button>Click me</button>
<!-- ❌ BAD: Conflicting roles -->
<h1 role="button">Heading Button</h1>
<!-- ✅ GOOD: Use appropriate element -->
<button>Click me</button>
<!-- ❌ BAD: aria-hidden on focusable element -->
<button aria-hidden="true">Hidden but focusable</button>
<!-- ✅ GOOD: Hide properly or don't hide -->
<button hidden>Truly hidden</button>
<!-- ❌ BAD: Empty aria-label -->
<button aria-label="">Click</button>
<!-- ✅ GOOD: Meaningful label -->
<button aria-label="Submit form">Submit</button>
<!-- ❌ BAD: Incorrect role usage -->
<div role="table">This is not really a table</div>
<!-- ✅ GOOD: Use proper table markup -->
<table><!-- proper table structure --></table>Key Takeaways
- ARIA enhances accessibility for dynamic content and custom widgets
- Always prefer native HTML elements over ARIA when possible
- ARIA has three components: roles, properties, and states
- Common attributes include aria-label, aria-labelledby, aria-describedby, and aria-hidden
- Use aria-live for dynamic content that needs to be announced
- Landmark roles help users navigate page sections
- Update ARIA states when your UI changes with JavaScript
- Test with actual screen readers to verify ARIA works correctly
What's Next?
Now that you understand ARIA attributes, you can make complex web applications accessible. In the next lesson, we'll explore keyboard navigation patterns and how to ensure all functionality is accessible without a mouse.