Web Accessibility (a11y) Guide
Web Accessibility (a11y) ensures that websites are usable by people of all abilities and disabilities. It involves best practices, semantic HTML, ARIA roles, attributes, and tools to provide an inclusive user experience.
Why Accessibility Matters
- Legal Compliance: WCAG, ADA, Section 508.
- Better UX: Improves usability for all users.
- SEO Benefits: Search engines prioritize accessible content.
- Inclusivity: Everyone, including people with disabilities, can use the web effectively.
Web Content Accessibility Guidelines (WCAG)
WCAG defines four principles:
- Perceivable: Users must be able to perceive the content.
- Operable: Navigation must be possible via keyboard and assistive devices.
- Understandable: Content should be clear and predictable.
- Robust: Compatible with various assistive technologies.
ARIA (Accessible Rich Internet Applications)
ARIA improves accessibility for dynamic content and advanced user interface controls.
ARIA Roles
Roles provide meaning to elements:
role="button"
– Identifies an element as a button.role="alert"
– Alerts users to important messages.role="dialog"
– Defines a modal dialog.role="navigation"
– Denotes a navigation region.role="tooltip"
– Provides extra information.role="combobox"
– Used for autocomplete inputs.
ARIA States & Properties
aria-expanded="true"
– Indicates if an element is expanded.aria-hidden="true"
– Hides an element from screen readers.aria-live="polite"
– Announces dynamic changes.aria-describedby="id"
– Associates description text.aria-label="label"
– Provides an accessible label.
ARIA Attributes
ARIA attributes enhance accessibility by providing additional context:
aria-labelledby="id"
– Associates an element with a label.aria-checked="true"
– Indicates a checked state in checkboxes and radio buttons.aria-disabled="true"
– Marks an element as disabled.aria-required="true"
– Denotes a required field.aria-selected="true"
– Highlights selected elements in a list or grid.aria-invalid="true"
– Indicates invalid input fields.aria-pressed="true"
– Represents a toggle button's state.aria-busy="true"
– Signals that an element is loading or processing.aria-multiselectable="true"
– Allows multiple selections in listboxes.
Common Accessibility Practices
Semantic HTML
Use elements appropriately:
<button>Submit</button> <!-- Correct -->
<div onclick="submit()">Submit</div> <!-- Incorrect -->
Skip Links
Provide a way to skip to content:
<a href="#main-content" class="skip-link">Skip to main content</a>
<main id="main-content">Content here...</main>
Focus Management
const button = document.querySelector("#open-dialog");
button.addEventListener("click", () => {
document.querySelector("#dialog").focus();
});
Accessible Widgets
Accessible Typeahead (Autocomplete)
<input type="text" role="combobox" aria-expanded="false" aria-autocomplete="list">
<ul role="listbox">
<li role="option">Option 1</li>
</ul>
Accessible Accordion
<button aria-expanded="false" aria-controls="panel1" id="accordion1">Toggle</button>
<div id="panel1" role="region" aria-labelledby="accordion1" hidden>
Panel Content
</div>
Accessible Tooltip
<button aria-describedby="tooltip1">Hover me</button>
<div id="tooltip1" role="tooltip" hidden>Tooltip content</div>
Accessible Combobox
<input type="text" role="combobox" aria-controls="listbox1" aria-expanded="false">
<ul id="listbox1" role="listbox" hidden>
<li role="option">Item 1</li>
</ul>
Accessible Dropdown
<button aria-haspopup="true" aria-expanded="false" id="menu-button">Menu</button>
<ul role="menu" hidden>
<li role="menuitem">Option 1</li>
</ul>
Accessible Pagination
<nav aria-label="Pagination">
<ul>
<li><a href="#" aria-label="Previous Page">Prev</a></li>
<li><a href="#">1</a></li>
<li><a href="#">2</a></li>
<li><a href="#" aria-label="Next Page">Next</a></li>
</ul>
</nav>
Accessibility Testing Tools
- Lighthouse: Google Chrome DevTools.
- axe DevTools: Browser extension for testing.
- NVDA, JAWS: Screen readers.
- WAVE: Web accessibility evaluation tool.
- tota11y: Accessibility visualization.
Alert/Notification Pattern
<!-- Live Region for Dynamic Content -->
<div
role="alert"
aria-live="assertive"
aria-atomic="true"
>
Error: Failed to save changes
</div>
<!-- Status Message (Less Urgent) -->
<div
role="status"
aria-live="polite"
aria-atomic="true"
>
Changes saved successfully
</div>
Modal Dialog
<div
role="dialog"
aria-labelledby="dialog-title"
aria-modal="true"
aria-describedby="dialog-desc"
>
<h2 id="dialog-title">Confirm Action</h2>
<p id="dialog-desc">Are you sure you want to proceed?</p>
<!-- Focus trap needed in JS -->
<button autofocus>Confirm</button>
<button>Cancel</button>
</div>
<script>
// Key handling
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') closeDialog();
if (e.key === 'Tab') manageFocusTrap(e);
});
</script>
Tabs
<div class="tabs">
<div role="tablist" aria-label="Programming Options">
<button
role="tab"
aria-selected="true"
aria-controls="panel-1"
id="tab-1"
tabindex="0"
>
First Tab
</button>
<button
role="tab"
aria-selected="false"
aria-controls="panel-2"
id="tab-2"
tabindex="-1"
>
Second Tab
</button>
</div>
<div
role="tabpanel"
id="panel-1"
aria-labelledby="tab-1"
tabindex="0"
>
Panel 1 content
</div>
<div
role="tabpanel"
id="panel-2"
aria-labelledby="tab-2"
tabindex="0"
hidden
>
Panel 2 content
</div>
</div>
<script>
// Key handling for tabs
document.addEventListener('keydown', (e) => {
switch(e.key) {
case 'ArrowLeft':
// Focus previous tab
break;
case 'ArrowRight':
// Focus next tab
break;
case 'Home':
// Focus first tab
break;
case 'End':
// Focus last tab
break;
}
});
</script>
Switch/Toggle
<button
role="switch"
aria-checked="false"
aria-label="Dark mode"
>
<span class="switch-thumb"></span>
<span class="switch-track"></span>
</button>
<script>
// Handle both click and keyboard events
button.addEventListener('click', toggle);
button.addEventListener('keydown', (e) => {
if (e.key === ' ' || e.key === 'Enter') toggle();
});
function toggle() {
const checked = button.getAttribute('aria-checked') === 'true';
button.setAttribute('aria-checked', !checked);
}
</script>
Dropdown/Combobox
<div class="combobox">
<label id="combo-label">Select an option</label>
<input
type="text"
role="combobox"
aria-expanded="false"
aria-controls="listbox-1"
aria-labelledby="combo-label"
aria-autocomplete="list"
/>
<ul
id="listbox-1"
role="listbox"
aria-labelledby="combo-label"
>
<li
role="option"
aria-selected="false"
id="option-1"
>
Option 1
</li>
<!-- More options -->
</ul>
</div>
Menu Button
<div class="menu">
<button
aria-haspopup="true"
aria-expanded="false"
aria-controls="menu-items"
>
Menu
</button>
<ul
id="menu-items"
role="menu"
hidden
>
<li>
<button role="menuitem">Option 1</button>
</li>
<li>
<button role="menuitem">Option 2</button>
</li>
</ul>
</div>
<script>
// Key handling
document.addEventListener('keydown', (e) => {
switch(e.key) {
case 'ArrowDown':
case 'ArrowUp':
// Navigate menu items
break;
case 'Escape':
// Close menu
break;
case 'Enter':
case ' ':
// Select item
break;
}
});
</script>
Accordion
<div class="accordion">
<h3>
<button
aria-expanded="false"
aria-controls="sect1"
class="accordion-trigger"
>
Section 1
</button>
</h3>
<div
id="sect1"
role="region"
aria-labelledby="accordion1"
hidden
>
<!-- Content -->
</div>
</div>
Key Principles
-
Keyboard Navigation:
- All interactive elements must be focusable
- Logical tab order (tabindex="0" or natural order)
- Visible focus indicators
- Handle both mouse and keyboard events
-
ARIA States:
- aria-expanded: For expandable elements
- aria-selected: For selected items
- aria-checked: For toggles/switches
- aria-hidden: To hide decorative elements
- aria-pressed: For toggle buttons
-
Screen Reader Support:
- Meaningful labels (aria-label, aria-labelledby)
- Descriptive text (aria-describedby)
- Role definitions
- Status updates (aria-live)
-
Focus Management:
- Trap focus in modals
- Return focus after closing dialogs
- Skip links for main content
- Focus visible (:focus-visible)
-
Common Keyboard Interactions:
- Enter/Space: Activate buttons
- Escape: Close dialogs/modals
- Arrow keys: Navigate menus/lists
- Tab: Move between focusable elements
- Home/End: Jump to start/end