Implement ARIA attributes, labels, and roles to create accessible web content for all users
ARIA (Accessible Rich Internet Applications) provides semantic information about elements to assistive technologies when HTML semantics aren't enough. While semantic HTML should always be your first choice, ARIA fills crucial gaps for complex interactive interfaces.
ARIA doesn't change how elements look or behave—it only affects the accessibility tree that assistive technologies read. Think of ARIA as a bridge between your interactive web applications and users who rely on screen readers, voice control, or other assistive devices.
Roles define what an element is or does. They tell assistive technologies how to interpret and interact with elements:
Landmark Roles:
Widget Roles:
Use roles sparingly—prefer semantic HTML when possible. Only add roles when HTML doesn't provide the semantics you need.
Properties describe element relationships and characteristics that don't change often:
Labeling Properties:
States describe current conditions and change during user interaction:
Always update states dynamically as users interact with your interface!
Proper labeling is crucial for accessibility. Follow these guidelines:
<button aria-label="Close dialog">×</button>
<h2 id="settings">Account Settings</h2>
<div aria-labelledby="settings">...</div>
<input aria-describedby="pwd-help">
<div id="pwd-help">Password must be 8+ characters</div>
<button aria-label="Save document" aria-describedby="save-status">
Save
</button>
<div id="save-status">Last saved 2 minutes ago</div>
Build a complete modal dialog with proper ARIA attributes, keyboard navigation, and focus management that works perfectly with screen readers.
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>Accessible Modal Dialog with ARIA</title>
7 <style>
8 /* Reset and base styles */
9 * { margin: 0; padding: 0; box-sizing: border-box; }
10
11 body {
12 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
13 line-height: 1.6;
14 padding: 20px;
15 background: #f5f7fa;
16 }
17
18 .container {
19 max-width: 800px;
20 margin: 0 auto;
21 background: white;
22 padding: 2rem;
23 border-radius: 12px;
24 box-shadow: 0 4px 12px rgba(0,0,0,0.1);
25 }
26
27 h1 {
28 color: #2c3e50;
29 margin-bottom: 1rem;
30 }
31
32 p {
33 color: #5a6c7d;
34 margin-bottom: 1.5rem;
35 }
36
37 /* Button styles */
38 .btn {
39 background: #3498db;
40 color: white;
41 border: none;
42 padding: 12px 24px;
43 border-radius: 6px;
44 cursor: pointer;
45 font-size: 16px;
46 font-weight: 500;
47 margin: 8px;
48 transition: all 0.2s ease;
49 }
50
51 .btn:hover, .btn:focus {
52 background: #2980b9;
53 transform: translateY(-1px);
54 box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
55 outline: 2px solid #3498db;
56 outline-offset: 2px;
57 }
58
59 .btn-success { background: #27ae60; }
60 .btn-success:hover, .btn-success:focus { background: #229954; outline-color: #27ae60; }
61
62 .btn-danger { background: #e74c3c; }
63 .btn-danger:hover, .btn-danger:focus { background: #c0392b; outline-color: #e74c3c; }
64
65 .btn-secondary { background: #95a5a6; }
66 .btn-secondary:hover, .btn-secondary:focus { background: #7f8c8d; outline-color: #95a5a6; }
67
68 /* Modal overlay */
69 .modal-overlay {
70 position: fixed;
71 top: 0;
72 left: 0;
73 width: 100%;
74 height: 100%;
75 background: rgba(0, 0, 0, 0.6);
76 backdrop-filter: blur(4px);
77 display: none;
78 align-items: center;
79 justify-content: center;
80 z-index: 1000;
81 animation: fadeIn 0.3s ease;
82 }
83
84 .modal-overlay.show {
85 display: flex;
86 }
87
88 @keyframes fadeIn {
89 from { opacity: 0; }
90 to { opacity: 1; }
91 }
92
93 @keyframes slideUp {
94 from {
95 opacity: 0;
96 transform: translate(-50%, -40%) scale(0.9);
97 }
98 to {
99 opacity: 1;
100 transform: translate(-50%, -50%) scale(1);
101 }
102 }
103
104 /* Modal dialog */
105 .modal {
106 position: absolute;
107 top: 50%;
108 left: 50%;
109 transform: translate(-50%, -50%);
110 background: white;
111 padding: 2rem;
112 border-radius: 12px;
113 box-shadow: 0 20px 40px rgba(0,0,0,0.3);
114 max-width: 500px;
115 width: 90vw;
116 max-height: 80vh;
117 overflow-y: auto;
118 animation: slideUp 0.3s ease;
119 }
120
121 .modal h2 {
122 color: #2c3e50;
123 margin-bottom: 1rem;
124 font-size: 1.5rem;
125 }
126
127 .modal p {
128 margin-bottom: 1.5rem;
129 color: #34495e;
130 }
131
132 /* Close button */
133 .close-btn {
134 position: absolute;
135 top: 1rem;
136 right: 1rem;
137 background: #e74c3c;
138 color: white;
139 border: none;
140 width: 32px;
141 height: 32px;
142 border-radius: 50%;
143 cursor: pointer;
144 font-size: 18px;
145 line-height: 1;
146 transition: all 0.2s ease;
147 }
148
149 .close-btn:hover, .close-btn:focus {
150 background: #c0392b;
151 transform: scale(1.1);
152 outline: 2px solid #e74c3c;
153 outline-offset: 2px;
154 }
155
156 /* Modal actions */
157 .modal-actions {
158 display: flex;
159 gap: 1rem;
160 justify-content: flex-end;
161 margin-top: 2rem;
162 padding-top: 1rem;
163 border-top: 1px solid #eee;
164 }
165
166 /* Focus trap styles */
167 .modal input, .modal textarea {
168 width: 100%;
169 padding: 12px;
170 border: 2px solid #ddd;
171 border-radius: 6px;
172 margin-bottom: 1rem;
173 font-size: 16px;
174 }
175
176 .modal input:focus, .modal textarea:focus {
177 outline: none;
178 border-color: #3498db;
179 box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
180 }
181
182 /* Visually hidden class for screen readers */
183 .visually-hidden {
184 position: absolute !important;
185 width: 1px;
186 height: 1px;
187 padding: 0;
188 margin: -1px;
189 overflow: hidden;
190 clip: rect(0, 0, 0, 0);
191 white-space: nowrap;
192 border: 0;
193 }
194
195 /* Live region for announcements */
196 .live-region {
197 position: absolute;
198 left: -10000px;
199 width: 1px;
200 height: 1px;
201 overflow: hidden;
202 }
203
204 /* Demo styles */
205 .demo-section {
206 background: #f8f9fa;
207 padding: 2rem;
208 border-radius: 8px;
209 margin: 2rem 0;
210 border-left: 4px solid #3498db;
211 }
212
213 .demo-section h3 {
214 color: #2c3e50;
215 margin-bottom: 1rem;
216 }
217 </style>
218</head>
219<body>
220 <div class="container">
221 <h1>🎯 Accessible Modal Dialog with ARIA</h1>
222 <p>This example demonstrates how to create fully accessible modal dialogs using proper ARIA attributes, focus management, and keyboard support.</p>
223
224 <div class="demo-section">
225 <h3>Try These Accessible Modals:</h3>
226 <p>Each modal demonstrates different ARIA patterns and accessibility features. Try navigating with Tab, Shift+Tab, and Escape keys.</p>
227
228 <button class="btn" id="openConfirmModal">
229 Open Confirmation Dialog
230 </button>
231
232 <button class="btn btn-success" id="openFormModal">
233 Open Contact Form Modal
234 </button>
235
236 <button class="btn btn-secondary" id="openInfoModal">
237 Open Information Modal
238 </button>
239 </div>
240 </div>
241
242 <!-- Live region for screen reader announcements -->
243 <div id="liveRegion" class="live-region" aria-live="polite" aria-atomic="true"></div>
244
245 <!-- Confirmation Modal -->
246 <div class="modal-overlay" id="confirmModalOverlay" aria-hidden="true">
247 <div class="modal"
248 role="dialog"
249 aria-modal="true"
250 aria-labelledby="confirmModalTitle"
251 aria-describedby="confirmModalDesc">
252
253 <button class="close-btn"
254 id="confirmCloseBtn"
255 aria-label="Close confirmation dialog">
256 ×
257 </button>
258
259 <h2 id="confirmModalTitle">Confirm Action</h2>
260 <p id="confirmModalDesc">
261 Are you sure you want to delete this item? This action cannot be undone and will permanently remove the selected content.
262 </p>
263
264 <div class="modal-actions" role="group" aria-labelledby="confirmActionsLabel">
265 <span id="confirmActionsLabel" class="visually-hidden">Confirmation Actions</span>
266 <button class="btn btn-secondary" id="cancelBtn">
267 Cancel
268 </button>
269 <button class="btn btn-danger" id="confirmBtn" aria-describedby="confirmWarning">
270 Delete Item
271 </button>
272 <span id="confirmWarning" class="visually-hidden">Warning: This will permanently delete the item</span>
273 </div>
274 </div>
275 </div>
276
277 <!-- Contact Form Modal -->
278 <div class="modal-overlay" id="formModalOverlay" aria-hidden="true">
279 <div class="modal"
280 role="dialog"
281 aria-modal="true"
282 aria-labelledby="formModalTitle">
283
284 <button class="close-btn"
285 id="formCloseBtn"
286 aria-label="Close contact form">
287 ×
288 </button>
289
290 <h2 id="formModalTitle">Contact Us</h2>
291
292 <form id="contactForm" novalidate>
293 <div>
294 <label for="contactName">Your Name (required):</label>
295 <input type="text"
296 id="contactName"
297 name="name"
298 required
299 aria-describedby="nameError"
300 aria-invalid="false">
301 <div id="nameError" role="alert" class="visually-hidden"></div>
302 </div>
303
304 <div>
305 <label for="contactEmail">Email Address (required):</label>
306 <input type="email"
307 id="contactEmail"
308 name="email"
309 required
310 aria-describedby="emailError emailHelp"
311 aria-invalid="false">
312 <div id="emailHelp" class="visually-hidden">We'll never share your email with anyone</div>
313 <div id="emailError" role="alert" class="visually-hidden"></div>
314 </div>
315
316 <div>
317 <label for="contactMessage">Your Message (required):</label>
318 <textarea id="contactMessage"
319 name="message"
320 rows="4"
321 required
322 aria-describedby="messageError messageHelp"
323 aria-invalid="false"
324 placeholder="Tell us how we can help you..."></textarea>
325 <div id="messageHelp" class="visually-hidden">Please provide as much detail as possible</div>
326 <div id="messageError" role="alert" class="visually-hidden"></div>
327 </div>
328
329 <div class="modal-actions">
330 <button type="button" class="btn btn-secondary" id="formCancelBtn">
331 Cancel
332 </button>
333 <button type="submit" class="btn btn-success">
334 Send Message
335 </button>
336 </div>
337 </form>
338 </div>
339 </div>
340
341 <!-- Information Modal -->
342 <div class="modal-overlay" id="infoModalOverlay" aria-hidden="true">
343 <div class="modal"
344 role="dialog"
345 aria-modal="true"
346 aria-labelledby="infoModalTitle"
347 aria-describedby="infoModalContent">
348
349 <button class="close-btn"
350 id="infoCloseBtn"
351 aria-label="Close information dialog">
352 ×
353 </button>
354
355 <h2 id="infoModalTitle">📚 ARIA Best Practices</h2>
356 <div id="infoModalContent">
357 <p>This modal demonstrates several ARIA accessibility patterns:</p>
358 <ul>
359 <li><strong>role="dialog"</strong> - Identifies this as a dialog</li>
360 <li><strong>aria-modal="true"</strong> - Indicates this is modal</li>
361 <li><strong>aria-labelledby</strong> - Points to the dialog title</li>
362 <li><strong>aria-describedby</strong> - Points to the main content</li>
363 <li><strong>aria-hidden</strong> - Hides/shows from screen readers</li>
364 <li><strong>Focus trapping</strong> - Keeps focus within the modal</li>
365 <li><strong>Escape key</strong> - Closes the modal</li>
366 </ul>
367 </div>
368
369 <div class="modal-actions">
370 <button class="btn" id="infoOkBtn">
371 Got It!
372 </button>
373 </div>
374 </div>
375 </div>
376
377 <script>
378 class AccessibleModal {
379 constructor(triggerSelector, modalSelector) {
380 this.trigger = document.querySelector(triggerSelector);
381 this.overlay = document.querySelector(modalSelector);
382 this.modal = this.overlay.querySelector('.modal');
383 this.closeBtn = this.modal.querySelector('.close-btn');
384 this.liveRegion = document.getElementById('liveRegion');
385
386 this.previousFocus = null;
387 this.focusableElements = [];
388
389 this.init();
390 }
391
392 init() {
393 this.trigger.addEventListener('click', () => this.open());
394 this.closeBtn.addEventListener('click', () => this.close());
395 this.overlay.addEventListener('click', (e) => {
396 if (e.target === this.overlay) this.close();
397 });
398
399 // Keyboard event delegation
400 this.modal.addEventListener('keydown', (e) => this.handleKeyDown(e));
401 }
402
403 open() {
404 this.previousFocus = document.activeElement;
405 this.overlay.classList.add('show');
406 this.overlay.setAttribute('aria-hidden', 'false');
407 document.body.style.overflow = 'hidden';
408
409 // Update focusable elements
410 this.updateFocusableElements();
411
412 // Focus first focusable element
413 if (this.focusableElements.length > 0) {
414 this.focusableElements[0].focus();
415 }
416
417 // Announce to screen readers
418 this.announce('Dialog opened');
419 }
420
421 close() {
422 this.overlay.classList.remove('show');
423 this.overlay.setAttribute('aria-hidden', 'true');
424 document.body.style.overflow = '';
425
426 // Return focus
427 if (this.previousFocus) {
428 this.previousFocus.focus();
429 this.previousFocus = null;
430 }
431
432 // Announce to screen readers
433 this.announce('Dialog closed');
434 }
435
436 updateFocusableElements() {
437 const focusableSelectors = [
438 'button:not([disabled])',
439 'input:not([disabled])',
440 'textarea:not([disabled])',
441 'select:not([disabled])',
442 'a[href]',
443 '[tabindex]:not([tabindex="-1"])'
444 ];
445
446 this.focusableElements = Array.from(
447 this.modal.querySelectorAll(focusableSelectors.join(','))
448 );
449 }
450
451 handleKeyDown(e) {
452 if (e.key === 'Escape') {
453 e.preventDefault();
454 this.close();
455 return;
456 }
457
458 if (e.key === 'Tab') {
459 this.trapFocus(e);
460 }
461 }
462
463 trapFocus(e) {
464 if (this.focusableElements.length === 0) return;
465
466 const firstElement = this.focusableElements[0];
467 const lastElement = this.focusableElements[this.focusableElements.length - 1];
468
469 if (e.shiftKey) {
470 // Shift + Tab
471 if (document.activeElement === firstElement) {
472 e.preventDefault();
473 lastElement.focus();
474 }
475 } else {
476 // Tab
477 if (document.activeElement === lastElement) {
478 e.preventDefault();
479 firstElement.focus();
480 }
481 }
482 }
483
484 announce(message) {
485 this.liveRegion.textContent = message;
486 }
487 }
488
489 // Initialize modals
490 const confirmModal = new AccessibleModal('#openConfirmModal', '#confirmModalOverlay');
491 const formModal = new AccessibleModal('#openFormModal', '#formModalOverlay');
492 const infoModal = new AccessibleModal('#openInfoModal', '#infoModalOverlay');
493
494 // Additional event handlers
495 document.getElementById('cancelBtn').onclick = () => confirmModal.close();
496 document.getElementById('confirmBtn').onclick = () => {
497 alert('Item deleted successfully!');
498 confirmModal.close();
499 };
500
501 document.getElementById('formCancelBtn').onclick = () => formModal.close();
502 document.getElementById('infoOkBtn').onclick = () => infoModal.close();
503
504 // Form validation
505 document.getElementById('contactForm').onsubmit = (e) => {
506 e.preventDefault();
507
508 const name = document.getElementById('contactName');
509 const email = document.getElementById('contactEmail');
510 const message = document.getElementById('contactMessage');
511
512 let isValid = true;
513
514 // Validate name
515 if (!name.value.trim()) {
516 showError('nameError', 'Name is required');
517 name.setAttribute('aria-invalid', 'true');
518 isValid = false;
519 } else {
520 hideError('nameError');
521 name.setAttribute('aria-invalid', 'false');
522 }
523
524 // Validate email
525 if (!email.value.trim()) {
526 showError('emailError', 'Email is required');
527 email.setAttribute('aria-invalid', 'true');
528 isValid = false;
529 } else if (!/\S+@\S+\.\S+/.test(email.value)) {
530 showError('emailError', 'Please enter a valid email address');
531 email.setAttribute('aria-invalid', 'true');
532 isValid = false;
533 } else {
534 hideError('emailError');
535 email.setAttribute('aria-invalid', 'false');
536 }
537
538 // Validate message
539 if (!message.value.trim()) {
540 showError('messageError', 'Message is required');
541 message.setAttribute('aria-invalid', 'true');
542 isValid = false;
543 } else {
544 hideError('messageError');
545 message.setAttribute('aria-invalid', 'false');
546 }
547
548 if (isValid) {
549 alert('Message sent successfully!');
550 formModal.close();
551 e.target.reset();
552 }
553 };
554
555 function showError(errorId, message) {
556 const errorEl = document.getElementById(errorId);
557 errorEl.textContent = message;
558 errorEl.classList.remove('visually-hidden');
559 errorEl.style.color = '#e74c3c';
560 errorEl.style.fontSize = '0.9rem';
561 errorEl.style.marginTop = '0.25rem';
562 }
563
564 function hideError(errorId) {
565 const errorEl = document.getElementById(errorId);
566 errorEl.textContent = '';
567 errorEl.classList.add('visually-hidden');
568 }
569 </script>
570</body>
571</html>This comprehensive example demonstrates advanced ARIA patterns including proper dialog roles, focus management, keyboard navigation, live regions for screen reader announcements, and form validation with accessibility in mind.
Excellent progress with ARIA attributes! You now know how to make complex interactive interfaces accessible to all users. Next, we'll focus on creating accessible forms and controls, diving deep into proper labeling, error handling, and keyboard navigation patterns.
Key Skills Mastered:
Coming Next: Accessible Forms & Controls - Master form accessibility with proper labels, validation, and error handling techniques.
Your progress is saved in this browser, on this device only.