Accessible Rich Internet Applications (ARIA) is a set of attributes that define ways to make web content and web applications more accessible to people with disabilities. ARIA supplements HTML so that semantic information can be provided when HTML semantics are not sufficient, enabling assistive technologies to convey appropriate information to users. While semantic HTML should always be the first choice, ARIA provides powerful tools for creating accessible dynamic content and complex user interfaces.
What is ARIA?
Definition and Purpose
ARIA (Accessible Rich Internet Applications) is a technical specification that provides a framework for adding accessibility information to web elements that cannot be adequately expressed with native HTML alone. It bridges the gap between complex web applications and assistive technologies, ensuring that all users can understand and interact with dynamic content.
When to Use ARIA
- Dynamic Content: When content changes without page reload
- Custom Controls: When creating non-standard UI elements
- Complex Widgets: For tabs, menus, dialogs, and trees
- Landmark Navigation: When HTML5 semantic elements aren't sufficient
- State Management: To communicate element states to assistive technologies
- Relationship Definition: To connect related elements
ARIA Categories
<!-- ARIA Categories -->
<!-- Roles: Define what an element is -->
<div role="navigation">...</div>
<!-- Properties: Define characteristics of an element -->
<input aria-label="Search products">
<!-- States: Define current condition of an element -->
<button aria-expanded="false">Menu</button>
ARIA Roles
ARIA roles define the purpose and behavior of elements for assistive technologies:
Landmark Roles
Landmark roles help users navigate through different sections of a page:
<!-- Landmark roles for page structure -->
<header role="banner">
<h1>Site Header</h1>
</header>
<nav role="navigation" aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<main role="main">
<h1>Main Content</h1>
<p>Primary page content...</p>
</main>
<aside role="complementary" aria-label="Sidebar">
<h2>Related Information</h2>
<p>Secondary content...</p>
</aside>
<footer role="contentinfo">
<p>Copyright and legal information</p>
</footer>
Widget Roles
The widget roles define the purpose and behavior of interactive elements:
<!-- Common widget roles -->
<!-- Tablist -->
<div role="tablist">
<button role="tab" aria-selected="true">Tab 1</button>
<button role="tab" aria-selected="false">Tab 2</button>
</div>
<!-- Menu -->
<ul role="menu">
<li role="menuitem"><a href="#">Item 1</a></li>
<li role="menuitem"><a href="#">Item 2</a></li>
</ul>
<!-- Dialog -->
<div role="dialog" aria-modal="true" aria-labelledby="dialog-title">
<h2 id="dialog-title">Dialog Title</h2>
<p>Dialog content...</p>
</div>
Document Structure Roles
Document structure roles provide semantic meaning to content sections:
<!-- Document structure roles -->
<article role="article">
<h1>Article Title</h1>
<p>Article content...</p>
</article>
<section role="region" aria-labelledby="region-title">
<h2 id="region-title">Region Title</h2>
<p>Region content...</p>
</section>
<div role="group" aria-labelledby="group-title">
<h3 id="group-title">Group Title</h3>
<p>Grouped content...</p>
</div>
ARIA Properties
ARIA properties provide additional information about elements to assistive technologies:
Labeling Properties
ARIA labeling properties provide accessible names for elements:
<!-- aria-label: Direct label for an element -->
<button aria-label="Close dialog">ร</button>
<input type="search" aria-label="Search products">
<!-- aria-labelledby: Reference to another element's text -->
<div id="dialog-title">User Profile</div>
<div role="dialog" aria-labelledby="dialog-title">
<p>Dialog content...</p>
</div>
<!-- Multiple labels -->
<h2 id="form-title">Registration Form</h2>
<p id="form-desc">Create your account to get started</p>
<form aria-labelledby="form-title" aria-describedby="form-desc">
<!-- Form fields -->
</form>
Descriptive Properties
ARIA descriptive properties provide additional information about elements:
<!-- aria-describedby: Reference to descriptive text -->
<input type="email"
aria-describedby="email-help email-error">
<div id="email-help">Enter your work email address</div>
<div id="email-error" hidden>Invalid email format</div>
<!-- aria-placeholder: Accessible placeholder text -->
<input type="text"
aria-placeholder="Search for products...">
<!-- aria-roledescription: Custom role description -->
<div role="group" aria-roledescription="Product filter options">
<!-- Filter options -->
</div>
Relationship Properties
ARIA relationship properties define connections between elements:
<!-- aria-owns: Indicates ownership of elements -->
<div role="grid" aria-owns="grid-cell-1 grid-cell-2">
<!-- Grid content -->
</div>
<div id="grid-cell-1">Cell 1</div>
<div id="grid-cell-2">Cell 2</div>
<!-- aria-controls: Element controlled by another -->
<button aria-controls="menu-panel" aria-expanded="false">
Toggle Menu
</button>
<div id="menu-panel" hidden>
<!-- Menu content -->
</div>
<!-- aria-flowto: Suggests next reading order -->
<div id="section-1" aria-flowto="section-2">Section 1</div>
<div id="section-2">Section 2</div>
ARIA States
ARIA states provide information about the current condition of elements to assistive technologies:
Common ARIA States
States communicate the current condition of elements to assistive technologies:
<!-- aria-expanded: For collapsible content -->
<button aria-expanded="false" aria-controls="menu">
Menu
</button>
<ul id="menu" hidden>
<li><a href="#">Item 1</a></li>
</ul>
<!-- aria-selected: For selection in lists and tabs -->
<div role="tablist">
<button role="tab" aria-selected="true">Active Tab</button>
<button role="tab" aria-selected="false">Inactive Tab</button>
</div>
<!-- aria-checked: For checkboxes and radio buttons -->
<div role="checkbox" aria-checked="false" tabindex="0">
Option 1
</div>
<div role="checkbox" aria-checked="true" tabindex="0">
Option 2
</div>
<!-- aria-disabled: For disabled elements -->
<button aria-disabled="true">Disabled Button</button>
<!-- aria-hidden: To hide elements from assistive technologies -->
<div aria-hidden="true">Loading spinner...</div>
Dynamic St ate Updates
ARIA states can be updated dynamically using JavaScript:
<!-- JavaScript to update ARIA states -->
<script>
function toggleMenu(button, menu) {
const isExpanded = button.getAttribute('aria-expanded') === 'true';
// Update ARIA states
button.setAttribute('aria-expanded', !isExpanded);
menu.hidden = isExpanded;
// Update button text
button.textContent = isExpanded ? 'Show Menu' : 'Hide Menu';
}
// Usage
const menuButton = document.getElementById('menu-button');
const menuPanel = document.getElementById('menu-panel');
menuButton.addEventListener('click', () => {
toggleMenu(menuButton, menuPanel);
});
</script>
Live Regions
Live regions are areas of the page that contain dynamic content which should be announced to screen reader users:
What are Live Regions?
Live regions announce dynamic content changes to screen reader users without requiring focus:
Live Region Attributes
Live regions can be configured with the following attributes:
<!-- aria-live: Politely announces changes -->
<div aria-live="polite" id="status-messages">
<!-- Status messages appear here -->
</div>
<!-- aria-live="assertive": Immediately announces important changes -->
<div aria-live="assertive" id="error-messages">
<!-- Error messages appear here -->
</div>
<!-- aria-live="off": No announcements (default) -->
<div aria-live="off">No announcements</div>
<!-- aria-atomic: Whether entire region should be announced -->
<div aria-live="polite" aria-atomic="true">
<span id="status">Status: </span>
<span id="message">Ready</span>
</div>
<!-- aria-relevant: What changes should be announced -->
<div aria-live="polite" aria-relevant="additions text">
<!-- Only additions and text changes are announced -->
</div>
Practical Live Region Examples
Here are some common use cases for live regions:
<!-- Form validation messages -->
<form>
<label for="email">Email:</label>
<input type="email" id="email" required>
<div aria-live="polite" id="validation-message"></div>
</form>
<!-- Loading status -->
<button id="load-data">Load Data</button>
<div aria-live="polite" id="loading-status"></div>
<!-- Search results count -->
<input type="search" id="search" placeholder="Search...">
<div aria-live="polite" id="results-count"></div>
<script>
// Form validation
document.getElementById('email').addEventListener('blur', function() {
const validationMessage = document.getElementById('validation-message');
if (this.validity.valid) {
validationMessage.textContent = 'Email is valid';
} else {
validationMessage.textContent = 'Please enter a valid email address';
}
});
// Loading status
document.getElementById('load-data').addEventListener('click', function() {
const status = document.getElementById('loading-status');
status.textContent = 'Loading...';
setTimeout(() => {
status.textContent = 'Data loaded successfully';
}, 2000);
});
// Search results
document.getElementById('search').addEventListener('input', function() {
const resultsCount = document.getElementById('results-count');
const query = this.value;
if (query.length > 0) {
resultsCount.textContent = `Searching for "${query}"...`;
// Simulate search
setTimeout(() => {
resultsCount.textContent = `Found 5 results for "${query}"`;
}, 500);
} else {
resultsCount.textContent = '';
}
});
</script>
Common ARIA Patterns
Here are some common ARIA patterns used to create accessible user interfaces:
Accessible Tabs
The following example demonstrates how to implement accessible tabs using ARIA attributes:
<!-- ARIA tabs implementation -->
<div role="tablist" aria-label="Product Categories">
<button role="tab"
aria-selected="true"
aria-controls="electronics-panel"
id="electronics-tab">
Electronics
</button>
<button role="tab"
aria-selected="false"
aria-controls="clothing-panel"
id="clothing-tab">
Clothing
</button>
</div>
<div role="tabpanel"
id="electronics-panel"
aria-labelledby="electronics-tab"
tabindex="0">
<h3>Electronics</h3>
<p>Electronic products content...</p>
</div>
<div role="tabpanel"
id="clothing-panel"
aria-labelledby="clothing-tab"
tabindex="0"
hidden>
<h3>Clothing</h3>
<p>Clothing products content...</p>
</div>
Accessible Menu
The following example demonstrates how to implement an accessible menu using ARIA attributes:
<!-- ARIA menu implementation -->
<button id="menu-button"
aria-haspopup="true"
aria-expanded="false">
Menu
</button>
<ul role="menu"
aria-labelledby="menu-button"
id="menu-panel"
hidden>
<li role="none">
<a role="menuitem" href="#">Home</a>
</li>
<li role="none">
<a role="menuitem" href="#">About</a>
</li>
<li role="none">
<a role="menuitem" href="#">Contact</a>
</li>
</ul>
Accessible Dialog
The following example demonstrates how to implement an accessible dialog using ARIA attributes:
<!-- ARIA dialog implementation -->
<button id="open-dialog">Open Dialog</button>
<div role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
id="dialog"
hidden>
<h2 id="dialog-title">Confirmation</h2>
<p>Are you sure you want to delete this item?</p>
<button id="confirm-delete">Delete</button>
<button id="cancel-delete">Cancel</button>
</div>
Accessible Accordion
<!-- ARIA accordion implementation -->
<div class="accordion">
<h3>
<button aria-expanded="false"
aria-controls="section1-content">
Section 1
</button>
</h3>
<div id="section1-content" hidden>
<p>Section 1 content...</p>
</div>
<h3>
<button aria-expanded="false"
aria-controls="section2-content">
Section 2
</button>
</h3>
<div id="section2-content" hidden>
<p>Section 2 content...</p>
</div>
</div>
ARIA Best Practices
Here are some best practices for using ARIA attributes effectively:
First Rule of ARIA
Don't use ARIA if you can use native HTML!
Always prefer semantic HTML elements over ARIA roles:
<!-- Bad: Using ARIA instead of native HTML -->
<div role="button" tabindex="0">Click me</div>
<div role="navigation">...</div>
<div role="img" alt="Description">...</div>
<!-- Good: Using native HTML elements -->
<button>Click me</button>
<nav>...</nav>
<img src="image.jpg" alt="Description">
Advanced ARIA Techniques
Here are some advanced techniques for implementing ARIA attributes:
Complex Grid Implementation
The following example demonstrates how to implement an accessible grid using ARIA attributes:
<!-- ARIA grid for data tables -->
<div role="grid" aria-label="Product inventory">
<div role="rowgroup">
<div role="row" aria-rowindex="1">
<div role="columnheader" aria-colindex="1">Product</div>
<div role="columnheader" aria-colindex="2">Price</div>
<div role="columnheader" aria-colindex="3">Stock</div>
</div>
</div>
<div role="rowgroup">
<div role="row" aria-rowindex="2" aria-selected="false">
<div role="gridcell" aria-colindex="1">Laptop</div>
<div role="gridcell" aria-colindex="2">$999</div>
<div role="gridcell" aria-colindex="3">15</div>
</div>
<div role="row" aria-rowindex="3" aria-selected="true">
<div role="gridcell" aria-colindex="1">Mouse</div>
<div role="gridcell" aria-colindex="2">$25</div>
<div role="gridcell" aria-colindex="3">50</div>
</div>
</div>
</div>
Custom Component with Full ARIA Support
The following example demonstrates how to implement an accessible custom rating component using ARIA attributes:
<!-- Custom rating component -->
<div class="rating"
role="slider"
aria-label="Product rating"
aria-valuemin="1"
aria-valuemax="5"
aria-valuenow="3"
aria-valuetext="3 out of 5 stars"
tabindex="0">
<span class="stars">
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
<span aria-hidden="true"></span>
</span>
<span class="rating-text">3 out of 5</span>
</div>
<script>
class AccessibleRating {
constructor(element) {
this.element = element;
this.stars = element.querySelectorAll('.stars span');
this.ratingText = element.querySelector('.rating-text');
this.currentRating = 3;
this.maxRating = 5;
this.init();
}
init() {
this.stars.forEach((star, index) => {
star.addEventListener('click', () => this.setRating(index + 1));
star.addEventListener('mouseenter', () => this.previewRating(index + 1));
});
this.element.addEventListener('mouseleave', () => this.updateDisplay());
// Keyboard support
this.element.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') {
this.setRating(Math.max(1, this.currentRating - 1));
} else if (e.key === 'ArrowRight') {
this.setRating(Math.min(this.maxRating, this.currentRating + 1));
}
});
}
setRating(rating) {
this.currentRating = rating;
this.updateDisplay();
this.updateARIA();
}
previewRating(rating) {
this.updateStars(rating);
this.ratingText.textContent = `${rating} out of 5`;
}
updateDisplay() {
this.updateStars(this.currentRating);
this.ratingText.textContent = `${this.currentRating} out of 5`;
}
updateStars(rating) {
this.stars.forEach((star, index) => {
star.textContent = index < rating ? 'star' : 'star_border';
});
}
updateARIA() {
this.element.setAttribute('aria-valuenow', this.currentRating);
this.element.setAttribute('aria-valuetext', `${this.currentRating} out of 5 stars`);
}
}
// Initialize rating component
const ratingElement = document.querySelector('.rating');
new AccessibleRating(ratingElement);
</script>
ARIA and JavaScript Integration
Here are some advanced techniques for implementing ARIA attributes:
Managing Focus
Proper focus management is crucial for accessibility, especially in dynamic content like dialogs:
<!-- Focus management for dialogs -->
<script>
class AccessibleDialog {
constructor(dialogElement) {
this.dialog = dialogElement;
this.previousFocus = null;
this.focusableElements = null;
}
open() {
// Store previous focus
this.previousFocus = document.activeElement;
// Show dialog
this.dialog.hidden = false;
this.dialog.setAttribute('aria-modal', 'true');
// Get focusable elements
this.focusableElements = this.dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
// Focus first element
if (this.focusableElements.length > 0) {
this.focusableElements[0].focus();
}
// Trap focus
this.trapFocus();
}
close() {
// Hide dialog
this.dialog.hidden = true;
this.dialog.removeAttribute('aria-modal');
// Restore previous focus
if (this.previousFocus) {
this.previousFocus.focus();
}
// Remove focus trap
this.removeFocusTrap();
}
trapFocus() {
this.focusTrapHandler = (e) => {
if (this.dialog.contains(e.target)) return;
const firstElement = this.focusableElements[0];
const lastElement = this.focusableElements[this.focusableElements.length - 1];
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
};
document.addEventListener('keydown', this.focusTrapHandler);
}
removeFocusTrap() {
document.removeEventListener('keydown', this.focusTrapHandler);
}
}
</script>
Announcing Dynamic Changes
Use live regions to announce important dynamic changes to screen reader users:
<!-- Announcing content changes -->
<div aria-live="polite" id="announcements"></div>
<script>
class Announcer {
constructor(element) {
this.element = element;
}
announce(message, priority = 'polite') {
// Create temporary live region if needed
if (priority === 'assertive') {
const assertiveRegion = document.createElement('div');
assertiveRegion.setAttribute('aria-live', 'assertive');
assertiveRegion.setAttribute('aria-atomic', 'true');
assertiveRegion.style.position = 'absolute';
assertiveRegion.style.left = '-10000px';
assertiveRegion.style.width = '1px';
assertiveRegion.style.height = '1px';
assertiveRegion.style.overflow = 'hidden';
document.body.appendChild(assertiveRegion);
assertiveRegion.textContent = message;
setTimeout(() => {
document.body.removeChild(assertiveRegion);
}, 1000);
} else {
this.element.textContent = message;
// Clear after announcement
setTimeout(() => {
this.element.textContent = '';
}, 1000);
}
}
}
// Usage
const announcer = new Announcer(document.getElementById('announcements'));
// Announce different events
document.getElementById('save-button').addEventListener('click', () => {
announcer.announce('Document saved successfully');
});
document.getElementById('delete-button').addEventListener('click', () => {
announcer.announce('Item deleted', 'assertive');
});
</script>
Complete ARIA Implementation Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ARIA Implementation Example</title>
<meta name="description" content="Complete ARIA implementation with tabs, dialogs, and live regions">
<style>
/* Basic styling */
body {
font-family: Arial, sans-serif;
line-height: 1.6;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
/* Focus styles */
*:focus {
outline: 3px solid #0066cc;
outline-offset: 2px;
}
/* Tab styles */
.tabs {
border: 1px solid #ccc;
border-radius: 4px;
}
.tablist {
display: flex;
border-bottom: 1px solid #ccc;
}
.tab {
background: none;
border: none;
padding: 12px 24px;
cursor: pointer;
border-bottom: 3px solid transparent;
}
.tab[aria-selected="true"] {
border-bottom-color: #0066cc;
font-weight: bold;
}
.tab:hover {
background-color: #f5f5f5;
}
.tabpanel {
padding: 20px;
min-height: 200px;
}
.tabpanel[hidden] {
display: none;
}
/* Dialog styles */
.dialog-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.dialog {
background: white;
padding: 24px;
border-radius: 8px;
max-width: 500px;
width: 100%;
}
.dialog[hidden] {
display: none;
}
/* Live region styles */
.live-region {
position: absolute;
left: -10000px;
width: 1px;
height: 1px;
overflow: hidden;
}
/* Button styles */
.btn {
padding: 8px 16px;
border: 1px solid #ccc;
background: white;
cursor: pointer;
border-radius: 4px;
}
.btn:hover {
background-color: #f5f5f5;
}
.btn-primary {
background: #0066cc;
color: white;
border-color: #0066cc;
}
.btn-primary:hover {
background: #0052a3;
}
</style>
</head>
<body>
<header role="banner">
<h1>ARIA Implementation Example</h1>
</header>
<main role="main">
<section aria-labelledby="tabs-heading">
<h2 id="tabs-heading">ARIA Tabs Example</h2>
<div class="tabs">
<div role="tablist" aria-label="Product categories">
<button class="tab"
role="tab"
aria-selected="true"
aria-controls="electronics-panel"
id="electronics-tab">
Electronics
</button>
<button class="tab"
role="tab"
aria-selected="false"
aria-controls="clothing-panel"
id="clothing-tab">
Clothing
</button>
<button class="tab"
role="tab"
aria-selected="false"
aria-controls="books-panel"
id="books-tab">
Books
</button>
</div>
<div class="tabpanel"
role="tabpanel"
id="electronics-panel"
aria-labelledby="electronics-tab"
tabindex="0">
<h3>Electronics</h3>
<p>Browse our selection of electronic products including smartphones, laptops, and accessories.</p>
<ul>
<li>Smartphones starting at $299</li>
<li>Laptops with latest processors</li>
<li>Audio equipment and headphones</li>
</ul>
</div>
<div class="tabpanel"
role="tabpanel"
id="clothing-panel"
aria-labelledby="clothing-tab"
tabindex="0"
hidden>
<h3>Clothing</h3>
<p>Discover our fashion collection with styles for every occasion and season.</p>
<ul>
<li>Men's and women's apparel</li>
<li>Seasonal collections</li>
<li>Accessories and shoes</li>
</ul>
</div>
<div class="tabpanel"
role="tabpanel"
id="books-panel"
aria-labelledby="books-tab"
tabindex="0"
hidden>
<h3>Books</h3>
<p>Explore our vast library of books across all genres and categories.</p>
<ul>
<li>Fiction and non-fiction bestsellers</li>
<li>Educational and technical books</li>
<li>Children's books and comics</li>
</ul>
</div>
</div>
</section>
<section aria-labelledby="dialog-heading">
<h2 id="dialog-heading">ARIA Dialog Example</h2>
<button class="btn btn-primary" id="open-dialog">
Open Confirmation Dialog
</button>
<div class="dialog-overlay" id="dialog-overlay" hidden>
<div class="dialog"
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
id="dialog">
<h2 id="dialog-title">Confirm Action</h2>
<p id="dialog-description">
Are you sure you want to proceed with this action? This cannot be undone.
</p>
<div style="margin-top: 20px;">
<button class="btn btn-primary" id="confirm-action">
Confirm
</button>
<button class="btn" id="cancel-action" style="margin-left: 8px;">
Cancel
</button>
</div>
</div>
</div>
</section>
<section aria-labelledby="live-region-heading">
<h2 id="live-region-heading">Live Region Example</h2>
<button class="btn" id="trigger-message">
Trigger Status Message
</button>
<button class="btn" id="trigger-error" style="margin-left: 8px;">
Trigger Error Message
</button>
<div style="margin-top: 20px;">
<strong>Status Messages:</strong>
<div id="status-display" aria-live="polite" aria-atomic="true">
<!-- Status messages will appear here -->
</div>
</div>
</section>
</main>
<!-- Live regions for screen readers -->
<div class="live-region" aria-live="polite" id="polite-announcer"></div>
<div class="live-region" aria-live="assertive" id="assertive-announcer"></div>
<script>
// Tab implementation
class AccessibleTabs {
constructor(tablistElement) {
this.tablist = tablistElement;
this.tabs = Array.from(tablistElement.querySelectorAll('[role="tab"]'));
this.panels = this.tabs.map(tab =>
document.getElementById(tab.getAttribute('aria-controls'))
);
this.init();
}
init() {
this.tabs.forEach((tab, index) => {
tab.addEventListener('click', () => this.selectTab(index));
tab.addEventListener('keydown', (e) => this.handleKeydown(e, index));
});
}
selectTab(index) {
// Update tabs
this.tabs.forEach((tab, i) => {
tab.setAttribute('aria-selected', i === index);
});
// Update panels
this.panels.forEach((panel, i) => {
panel.hidden = i !== index;
});
// Focus selected tab
this.tabs[index].focus();
}
handleKeydown(e, currentIndex) {
let newIndex = currentIndex;
switch(e.key) {
case 'ArrowLeft':
newIndex = currentIndex > 0 ? currentIndex - 1 : this.tabs.length - 1;
break;
case 'ArrowRight':
newIndex = currentIndex < this.tabs.length - 1 ? currentIndex + 1 : 0;
break;
case 'Home':
newIndex = 0;
break;
case 'End':
newIndex = this.tabs.length - 1;
break;
default:
return;
}
e.preventDefault();
this.selectTab(newIndex);
}
}
// Dialog implementation
class AccessibleDialog {
constructor(dialogElement, triggerElement) {
this.dialog = dialogElement;
this.trigger = triggerElement;
this.overlay = dialogElement.parentElement;
this.previousFocus = null;
this.focusableElements = null;
this.init();
}
init() {
this.trigger.addEventListener('click', () => this.open());
const closeButtons = this.dialog.querySelectorAll('#cancel-action, #confirm-action');
closeButtons.forEach(button => {
button.addEventListener('click', () => this.close());
});
// Close on escape key
this.dialog.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
this.close();
}
});
}
open() {
this.previousFocus = document.activeElement;
this.overlay.hidden = false;
this.focusableElements = this.dialog.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (this.focusableElements.length > 0) {
this.focusableElements[0].focus();
}
this.trapFocus();
}
close() {
this.overlay.hidden = true;
if (this.previousFocus) {
this.previousFocus.focus();
}
this.removeFocusTrap();
}
trapFocus() {
this.focusTrapHandler = (e) => {
if (this.overlay.contains(e.target)) return;
const firstElement = this.focusableElements[0];
const lastElement = this.focusableElements[this.focusableElements.length - 1];
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
};
document.addEventListener('keydown', this.focusTrapHandler);
}
removeFocusTrap() {
document.removeEventListener('keydown', this.focusTrapHandler);
}
}
// Live region implementation
class LiveRegionManager {
constructor(politeRegion, assertiveRegion, displayRegion) {
this.politeRegion = politeRegion;
this.assertiveRegion = assertiveRegion;
this.displayRegion = displayRegion;
}
announce(message, priority = 'polite') {
// Update visual display
this.displayRegion.textContent = message;
// Announce to screen readers
if (priority === 'assertive') {
this.assertiveRegion.textContent = message;
setTimeout(() => {
this.assertiveRegion.textContent = '';
}, 1000);
} else {
this.politeRegion.textContent = message;
setTimeout(() => {
this.politeRegion.textContent = '';
}, 1000);
}
}
}
// Initialize components
document.addEventListener('DOMContentLoaded', () => {
// Initialize tabs
const tablist = document.querySelector('[role="tablist"]');
new AccessibleTabs(tablist);
// Initialize dialog
const dialog = document.getElementById('dialog');
const dialogTrigger = document.getElementById('open-dialog');
new AccessibleDialog(dialog, dialogTrigger);
// Initialize live regions
const politeRegion = document.getElementById('polite-announcer');
const assertiveRegion = document.getElementById('assertive-announcer');
const displayRegion = document.getElementById('status-display');
const liveRegionManager = new LiveRegionManager(politeRegion, assertiveRegion, displayRegion);
// Live region buttons
document.getElementById('trigger-message').addEventListener('click', () => {
liveRegionManager.announce('Status: Operation completed successfully');
});
document.getElementById('trigger-error').addEventListener('click', () => {
liveRegionManager.announce('Error: Operation failed. Please try again.', 'assertive');
});
});
</script>
</body>
</html>