Understanding ARIA
ARIA (Accessible Rich Internet Applications) is a set of attributes that define ways to make web content and applications more accessible to people with disabilities. Developed by the W3C, ARIA supplements HTML's native accessibility features by providing additional semantic information about elements and their behavior.
Think of ARIA as a language translator between your complex interface components and assistive technologies like screen readers. When HTML can't fully express the purpose or state of an element, ARIA helps fill in the gaps.
The First Rule of ARIA
If you can use a native HTML element or attribute with the semantics and behavior you require already built in, instead of re-purposing an element and adding an ARIA role, state or property to make it accessible, then do so.
HTML element be used?} B -->|Yes| C[Use native HTML element] B -->|No| D[Use appropriate ARIA] C --> E[Example: Use <button>
instead of <div role="button">] D --> F[Example: Add aria-expanded
to a custom dropdown]
ARIA Components
ARIA consists of three main components:
1. Roles
Roles define what an element is or does. They either reinforce the native semantics (e.g., role="button" on a <button>) or override them when necessary (e.g., role="button" on a <div>).
2. Properties
Properties describe characteristics of elements that are unlikely to change during the element's lifetime. For example, aria-label provides an accessible name for an element.
3. States
States describe the current condition of elements that are likely to change through user interaction. For example, aria-expanded="true" indicates that a collapsible element is currently expanded.
Important ARIA Roles
ARIA roles are divided into several categories. Here are some of the most commonly used roles:
Landmark Roles
Landmark roles identify regions of a page, helping users navigate between major sections. Ideally, you should use semantic HTML5 elements instead of landmark roles where possible.
<!-- HTML5 semantic element - preferred approach -->
<header>...</header>
<!-- Equivalent ARIA role - use when HTML5 element isn't possible -->
<div role="banner">...</div>
| Landmark Role | HTML5 Equivalent | Purpose |
|---|---|---|
banner |
<header> |
Site header containing site-oriented content |
navigation |
<nav> |
Collection of navigational links |
main |
<main> |
Main content of the document |
complementary |
<aside> |
Supporting content that remains meaningful when separated |
contentinfo |
<footer> |
Footer containing metadata, copyright information, etc. |
search |
No direct equivalent | Search functionality for the website |
form |
<form> (sometimes) |
Collection of form-associated elements |
Widget Roles
Widget roles define common interactive patterns that aren't fully expressed by native HTML elements.
Common Widget Roles
button: An interactive element that triggers an actioncheckbox: A checkable input that has three possible valuestablist,tab,tabpanel: Used together for a tabbed interfacecombobox: Input that controls another element that can dynamically pop up to help the usermenu,menuitem: A set of menu optionsprogressbar: Element that displays the progress of a taskslider: Input where the user selects a value from a range
Document Structure Roles
Document structure roles describe the organizational structure of content.
Common Document Structure Roles
article: A complete or self-contained compositiondefinition: A definition of a term or conceptdirectory: A list of references, such as a table of contentslist,listitem: Used for lists of itemspresentation: Element should be ignored by assistive technology (purely decorative)
ARIA Properties and States
ARIA properties and states provide additional information about elements and their behavior. Here are some of the most important ones:
Providing Accessible Names
<!-- Using aria-label -->
<button aria-label="Close dialog">×</button>
<!-- Using aria-labelledby to reference another element -->
<div id="heading">Weather Forecast</div>
<div role="region" aria-labelledby="heading">
<!-- Weather content -->
</div>
<!-- Using aria-describedby for additional description -->
<input type="password" aria-describedby="password-requirements">
<p id="password-requirements">Password must be at least 8 characters and include a number.</p>
These attributes work like signage in a building. aria-label is like a label directly on a door, aria-labelledby is like having a sign that points to a door, and aria-describedby is like having detailed instructions posted nearby.
Interactive Element States
<!-- Expanded state for dropdown menus, accordions, etc. -->
<button aria-expanded="false" aria-controls="dropdown-content">Show Options</button>
<ul id="dropdown-content" hidden>
<li>Option 1</li>
<li>Option 2</li>
</ul>
<!-- Selected state for custom controls -->
<div role="tablist">
<button role="tab" aria-selected="true" id="tab1">Tab 1</button>
<button role="tab" aria-selected="false" id="tab2">Tab 2</button>
</div>
<!-- Checked state for custom checkboxes or radio buttons -->
<div role="checkbox" aria-checked="true" tabindex="0">
Enable notifications
</div>
Other Important Properties and States
aria-hidden="true": Element is not visible to assistive technologiesaria-disabled="true": Element is perceivable but disabledaria-required="true": User input is required on the elementaria-current="page": Indicates the current item within a set (e.g., current page in navigation)aria-busy="true": Element is being modified and assistive technologies may want to waitaria-live="polite|assertive|off": Indicates how assistive technologies should announce updates
Common ARIA Patterns
Let's explore some common UI patterns and how ARIA can make them accessible:
Tabs
Tabs organize content into separate views where only one view is visible at a time.
<div class="tabs-container">
<div role="tablist" aria-label="Programming languages">
<button role="tab"
aria-selected="true"
aria-controls="panel-js"
id="tab-js">
JavaScript
</button>
<button role="tab"
aria-selected="false"
aria-controls="panel-python"
id="tab-python">
Python
</button>
<button role="tab"
aria-selected="false"
aria-controls="panel-php"
id="tab-php">
PHP
</button>
</div>
<div role="tabpanel"
id="panel-js"
aria-labelledby="tab-js">
JavaScript content here...
</div>
<div role="tabpanel"
id="panel-python"
aria-labelledby="tab-python"
hidden>
Python content here...
</div>
<div role="tabpanel"
id="panel-php"
aria-labelledby="tab-php"
hidden>
PHP content here...
</div>
</div>
In this example, ARIA creates relationships between tabs and their content panels, while communicating which tab is currently selected.
Accordions
Accordions allow users to expand and collapse sections of content.
<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>
Content for section 1...
</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>
Content for section 2...
</div>
</div>
ARIA attributes communicate which buttons control which content sections and whether sections are currently expanded.
Modal Dialogs
Modal dialogs present focused content that requires attention or interaction.
<!-- Button to open the dialog -->
<button onclick="openDialog()">Open Dialog</button>
<!-- The dialog itself -->
<div role="dialog"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc"
aria-modal="true"
id="my-dialog"
hidden>
<div role="document">
<h2 id="dialog-title">Confirm Action</h2>
<p id="dialog-desc">Are you sure you want to continue?</p>
<div class="dialog-buttons">
<button onclick="cancelDialog()">Cancel</button>
<button onclick="confirmDialog()">Confirm</button>
</div>
<button aria-label="Close dialog"
onclick="closeDialog()"
class="close-button">
×
</button>
</div>
</div>
Here, ARIA indicates that the element is a dialog, provides an accessible name and description, and indicates that it's modal (blocks interaction with the rest of the page).
Live Regions
Live regions are a special type of ARIA that allow dynamic content changes to be announced by screen readers. They're essential for updates that don't receive focus but need user attention.
Types of Live Regions
<!-- Polite announcements (won't interrupt current speech) -->
<div aria-live="polite">
Your settings have been saved successfully.
</div>
<!-- Assertive announcements (may interrupt current speech) -->
<div aria-live="assertive">
Error: Your session is about to expire in 30 seconds.
</div>
<!-- Role-based alternatives -->
<div role="status">
5 new messages received.
</div>
<div role="alert">
Form could not be submitted due to validation errors.
</div>
Think of "polite" announcements as someone waiting for a pause in conversation before speaking, while "assertive" announcements are like someone interrupting with urgent information.
Additional Live Region Attributes
aria-atomic="true|false": Whether the entire region should be announced as a whole when any part changesaria-relevant="additions|removals|text|all": What types of changes should be announcedaria-busy="true|false": Indicates the element is being updated
ARIA Best Practices and Common Mistakes
Best Practices
- Use HTML first: Always prefer native HTML elements when available
- Maintain relationships: Ensure control elements reference the elements they control
- Update ARIA states dynamically: Change
aria-expanded,aria-selected, etc. when state changes - Test with assistive technologies: Verify your ARIA works as expected with screen readers
- Keep it simple: Only add ARIA that provides necessary information
Common Mistakes
1. Redundant ARIA
<!-- Redundant: button already has role="button" implicitly -->
<button role="button">Click me</button>
<!-- Redundant: h1 already has implicit heading role -->
<h1 role="heading" aria-level="1">Page Title</h1>
2. Conflicting ARIA
<!-- Conflicting: changing the native semantics inappropriately -->
<h1 role="button">This is not a proper button</h1>
<!-- Conflicting: list item outside of a list -->
<div role="listitem">Item without a list parent</div>
3. Incomplete ARIA
<!-- Missing associated elements -->
<div role="tablist">
<div role="tab">Tab 1</div> <!-- Missing aria-controls -->
</div>
<div>Tab 1 content</div> <!-- Missing role="tabpanel" -->
<!-- Missing keyboard support for interactive elements -->
<div role="button">Submit</div> <!-- No tabindex or event handlers -->
4. Missing Key Functionality
<!-- Role without keyboard support -->
<div role="button" onclick="submitForm()">Submit</div>
<!-- Missing tabindex and keyboard event handlers -->
<!-- Fix: Add keyboard support -->
<div role="button"
onclick="submitForm()"
onkeydown="handleKeydown(event)"
tabindex="0">
Submit
</div>
<script>
function handleKeydown(event) {
// Handle Enter and Space keys
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
submitForm();
}
}
</script>
Real-World Examples
Custom Dropdown Menu
<div class="dropdown">
<button aria-haspopup="true"
aria-expanded="false"
aria-controls="dropdown-menu">
Settings
<span class="icon" aria-hidden="true">▼</span>
</button>
<ul id="dropdown-menu"
role="menu"
hidden>
<li role="menuitem">
<button>Profile</button>
</li>
<li role="menuitem">
<button>Account</button>
</li>
<li role="menuitem">
<button>Logout</button>
</li>
</ul>
</div>
<script>
const dropdown = document.querySelector('.dropdown button');
const menu = document.getElementById('dropdown-menu');
dropdown.addEventListener('click', () => {
const expanded = dropdown.getAttribute('aria-expanded') === 'true';
dropdown.setAttribute('aria-expanded', !expanded);
menu.hidden = expanded;
});
// Additional keyboard handling would be needed
</script>
Custom Form Validation
<form>
<div class="form-group">
<label for="email">Email Address</label>
<input type="email"
id="email"
aria-describedby="email-error"
aria-invalid="false">
<div id="email-error"
class="error-message"
role="alert"
hidden></div>
</div>
<button type="submit">Submit</button>
</form>
<script>
const emailInput = document.getElementById('email');
const emailError = document.getElementById('email-error');
emailInput.addEventListener('blur', () => {
if (!isValidEmail(emailInput.value)) {
emailInput.setAttribute('aria-invalid', 'true');
emailError.textContent = 'Please enter a valid email address';
emailError.hidden = false;
} else {
emailInput.setAttribute('aria-invalid', 'false');
emailError.hidden = true;
}
});
function isValidEmail(email) {
// Email validation logic here
return /\S+@\S+\.\S+/.test(email);
}
</script>
Loading Indicator
<button onclick="loadData()">Load Data</button>
<div id="loading-indicator" role="status" aria-live="polite" hidden>
Loading data, please wait...
</div>
<div id="results-container" aria-live="polite">
<!-- Results will be inserted here -->
</div>
<script>
function loadData() {
const loadingIndicator = document.getElementById('loading-indicator');
const resultsContainer = document.getElementById('results-container');
// Show loading message
loadingIndicator.hidden = false;
// Simulate data loading
setTimeout(() => {
// Hide loading message
loadingIndicator.hidden = true;
// Update results
resultsContainer.innerHTML = '<p>Data loaded successfully! 5 items found.</p>';
}, 2000);
}
</script>
Practical Exercise: Implementing ARIA
Exercise: Add ARIA to a Custom UI Component
Add appropriate ARIA roles and attributes to make this custom star rating component accessible:
<div class="star-rating">
<div class="rating-label">Rate this product:</div>
<div class="stars">
<span class="star" data-value="1">★</span>
<span class="star" data-value="2">★</span>
<span class="star" data-value="3">★</span>
<span class="star" data-value="4">★</span>
<span class="star" data-value="5">★</span>
</div>
<div class="rating-value">0/5</div>
</div>
<script>
const stars = document.querySelectorAll('.star');
const ratingValue = document.querySelector('.rating-value');
let currentRating = 0;
stars.forEach(star => {
star.addEventListener('click', () => {
const value = parseInt(star.getAttribute('data-value'));
setRating(value);
});
});
function setRating(value) {
currentRating = value;
stars.forEach(star => {
const starValue = parseInt(star.getAttribute('data-value'));
star.classList.toggle('selected', starValue <= currentRating);
});
ratingValue.textContent = `${currentRating}/5`;
}
</script>
Think about:
- What role should the stars have?
- How can you make the stars focusable and keyboard operable?
- How should you communicate the current rating to screen readers?
- What labels or descriptions are needed?
Summary and Key Takeaways
- Use HTML first: Only reach for ARIA when native HTML semantics aren't sufficient
- Understand ARIA components: Roles define what elements are, properties provide characteristics, and states describe current conditions
- Follow established patterns: Use common ARIA patterns for consistent accessibility
- Update ARIA dynamically: Change states like
aria-expandedwhen user interactions change the UI - Use live regions for dynamic content changes that need to be announced
- Test with real assistive technology: Verify your ARIA implementation works as expected
- Avoid common mistakes: Redundant, conflicting, or incomplete ARIA can make accessibility worse
Further Reading
Weekend Assignment
For this weekend's project, you'll build a multi-page website with semantic HTML, proper structure, and accessibility features. Your task is to:
- Create a 3-5 page website on a topic of your choice (portfolio, hobby site, fictional business, etc.)
- Implement proper semantic HTML structure throughout
- Create at least one complex UI component (tabs, accordion, modal, etc.) using ARIA
- Ensure all content is accessible, including proper heading structure, alt text, and ARIA where needed
- Test your site with keyboard navigation and a screen reader if possible
- Submit a document explaining your accessibility considerations and how you implemented them
Refer to the course materials and additional resources as needed. Good luck!