HTML Accessibility & ARIA Guide
Complete reference for HTML accessibility and ARIA (Accessible Rich Internet Applications). Learn roles, states, properties, semantic HTML patterns, and best practices for building accessible web applications.
Introduction
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. ARIA helps assistive technologies like screen readers understand the purpose and state of interactive elements.
What is ARIA?
Roles
Define what an element is or does
role="button"
States
Dynamic properties that change with user interaction
aria-expanded="true"
Properties
Attributes that describe relationships and characteristics
aria-label="Close"
Browser Support
Supported in all modern browsers and screen readers
Universal support
Important: ARIA doesn't change the behavior or appearance of elements—it only provides additional semantic information to assistive technologies. You must still implement keyboard interactions and visual states with JavaScript and CSS.
Five Rules of ARIA
Follow these fundamental rules when implementing ARIA in your applications.
| Rule | Description | Example |
|---|---|---|
| 1. Use Semantic HTML First | If a native HTML element has the semantics and behavior you need, use it instead of repurposing another element with ARIA. | Use <button> instead of <div role="button"> |
| 2. Don't Change Native Semantics | Don't override native HTML semantics unless you absolutely have to. | Bad: <h2 role="tab">Good: <div role="tab"> |
| 3. All Interactive Elements Must Be Keyboard Accessible | Users must be able to navigate to and interact with all elements using only the keyboard. | Add tabindex="0" to custom interactive elements |
| 4. Don't Use role="presentation" or aria-hidden on Focusable Elements | Never hide elements from assistive tech if they can receive keyboard focus. | Bad: <button aria-hidden="true"> |
| 5. All Interactive Elements Must Have Accessible Names | Provide text alternatives for interactive elements so users know what they do. | <button aria-label="Close dialog">×</button> |
Landmark Roles
Landmark roles identify regions of a page, allowing screen reader users to quickly navigate to different sections.
| Role | HTML Equivalent | Description | Example |
|---|---|---|---|
banner |
<header> |
Site-wide header (not within article/section) | <header role="banner"> |
navigation |
<nav> |
Collection of navigational links | <nav aria-label="Main"> |
main |
<main> |
Primary content of the page (use once per page) | <main role="main"> |
complementary |
<aside> |
Supporting content related to main content | <aside role="complementary"> |
contentinfo |
<footer> |
Site-wide footer (not within article/section) | <footer role="contentinfo"> |
search |
None | Search functionality | <form role="search"> |
form |
<form> |
Form landmark (with accessible name) | <form aria-label="Contact"> |
region |
<section> |
Important content area (requires label) | <section aria-labelledby="news"> |
Tip: Use semantic HTML5 elements instead of ARIA roles when possible. If you must support older browsers, include both (e.g., <main role="main">).
Document Structure Roles
Document structure roles describe the organization of content in the page.
| Role | HTML Equivalent | Description | Example |
|---|---|---|---|
article |
<article> |
Self-contained composition | <article role="article"> |
heading |
<h1>-<h6> |
Heading for a section (use aria-level) | <div role="heading" aria-level="2"> |
list |
<ul>, <ol> |
Group of non-interactive list items | <ul role="list"> |
listitem |
<li> |
Single item in a list | <li role="listitem"> |
table |
<table> |
Data table structure | <div role="table"> |
row |
<tr> |
Row of cells in a table/grid | <div role="row"> |
cell |
<td> |
Cell in a table/grid row | <div role="cell"> |
columnheader |
<th scope="col"> |
Header cell for a column | <div role="columnheader"> |
rowheader |
<th scope="row"> |
Header cell for a row | <div role="rowheader"> |
img |
<img> |
Image (use with aria-label) | <div role="img" aria-label="..."> |
figure |
<figure> |
Content with optional caption | <figure role="figure"> |
separator |
<hr> |
Divider between content sections | <hr role="separator"> |
Widget Roles
Widget roles define interactive components. These require keyboard interaction implementation and state management.
| Role | Description | Required States/Properties | Keyboard |
|---|---|---|---|
button |
Clickable button element | aria-pressed (if toggle) | Space, Enter |
link |
Hyperlink to another resource | None required | Enter |
checkbox |
Checkable input with three states | aria-checked (true/false/mixed) | Space |
radio |
Radio button in a group | aria-checked (true/false) | Arrow keys |
switch |
On/off toggle switch | aria-checked (true/false) | Space |
textbox |
Input for free-form text | aria-multiline, aria-readonly | Standard text input |
searchbox |
Search input field | None required | Standard text input |
slider |
Range selection input | aria-valuenow, aria-valuemin, aria-valuemax | Arrow keys, Home, End |
spinbutton |
Numeric input with increment/decrement | aria-valuenow, aria-valuemin, aria-valuemax | Arrow keys |
progressbar |
Progress indicator | aria-valuenow (or indeterminate) | Not focusable |
combobox |
Combined input and dropdown | aria-expanded, aria-controls | Arrow keys, Enter, Esc |
listbox |
List of selectable options | aria-activedescendant or option focus | Arrow keys, Home, End |
option |
Selectable item in listbox | aria-selected | Part of listbox navigation |
menu |
List of actions or functions | None required | Arrow keys, Esc |
menuitem |
Item in a menu | None required | Enter, Space |
menuitemcheckbox |
Checkable menu item | aria-checked | Enter, Space |
menuitemradio |
Radio option in menu | aria-checked | Enter, Space |
tab |
Tab in a tablist | aria-selected, aria-controls | Arrow keys, Home, End |
tablist |
Container for tabs | aria-orientation (optional) | Contains tab elements |
tabpanel |
Content panel for a tab | aria-labelledby | Not focusable by default |
dialog |
Modal or non-modal dialog | aria-modal, aria-labelledby | Esc (close), Tab (trap) |
alertdialog |
Dialog with important message | aria-modal, aria-labelledby, aria-describedby | Esc (close), Tab (trap) |
tooltip |
Contextual popup with information | None required | Esc (close) |
Important: Widget roles require full keyboard implementation and state management. Simply adding the role attribute is not enough—you must implement all expected behaviors with JavaScript.
Live Region Roles
Live region roles announce dynamic content changes to screen reader users without moving focus.
| Role | Politeness | Description | Use Case |
|---|---|---|---|
alert |
Assertive | Important, time-sensitive message | Error messages, urgent notifications |
status |
Polite | Advisory information for the user | Success messages, status updates |
log |
Polite | Sequential information (new items added) | Chat logs, history, activity feeds |
marquee |
Off (by default) | Non-essential scrolling information | Stock tickers, news tickers |
timer |
Off (by default) | Numerical counter or timer | Countdown timers, stopwatches |
Live Region Properties
| Property | Values | Description |
|---|---|---|
aria-live |
off, polite, assertive | Sets the priority of updates |
aria-atomic |
true, false | Whether to announce entire region or just changes |
aria-relevant |
additions, removals, text, all | What types of changes to announce |
aria-busy |
true, false | Whether region is currently being updated |
Best Practice: Use aria-live="polite" for most notifications. Reserve aria-live="assertive" for critical errors or urgent information that requires immediate attention.
ARIA States & Properties
ARIA states and properties provide additional semantic information and describe relationships between elements.
Labeling & Describing
| Attribute | Usage | Example |
|---|---|---|
aria-label |
Provides accessible name directly | <button aria-label="Close">×</button> |
aria-labelledby |
References element(s) that label this element | <div role="dialog" aria-labelledby="title"> |
aria-describedby |
References element(s) that describe this element | <input aria-describedby="password-help"> |
aria-placeholder |
Hint text when field is empty | <input aria-placeholder="YYYY-MM-DD"> |
Widget States
| Attribute | Values | Usage |
|---|---|---|
aria-checked |
true, false, mixed | State of checkboxes, radio buttons, switches |
aria-selected |
true, false | Whether item is selected in a group |
aria-pressed |
true, false, mixed | State of toggle buttons |
aria-expanded |
true, false | Whether element is expanded or collapsed |
aria-hidden |
true, false | Hides element from accessibility tree |
aria-disabled |
true, false | Indicates element is perceivable but disabled |
aria-readonly |
true, false | Indicates element is not editable |
aria-current |
page, step, location, date, time, true, false | Indicates current item in a set |
Relationships
| Attribute | Usage | Example |
|---|---|---|
aria-controls |
IDs of elements controlled by this element | <button aria-controls="panel1"> |
aria-owns |
IDs of elements owned by this element | <div aria-owns="item1 item2"> |
aria-activedescendant |
ID of currently active child element | <div role="listbox" aria-activedescendant="opt3"> |
aria-flowto |
ID of next element in reading order | <div aria-flowto="section2"> |
aria-details |
ID of element with extended description | <img aria-details="figure-caption"> |
Form Validation
| Attribute | Values | Usage |
|---|---|---|
aria-required |
true, false | Indicates field must be filled before submit |
aria-invalid |
true, false, grammar, spelling | Indicates input value is invalid |
aria-errormessage |
ID of error message element | <input aria-errormessage="email-error"> |
Values
| Attribute | Usage |
|---|---|
aria-valuenow |
Current numeric value for range widgets |
aria-valuemin |
Minimum value for range widgets |
aria-valuemax |
Maximum value for range widgets |
aria-valuetext |
Human-readable alternative to aria-valuenow |
Semantic HTML First
Native HTML elements provide built-in accessibility. Always use semantic HTML before adding ARIA.
Comparison: Semantic HTML vs ARIA
| Semantic HTML (Preferred) | ARIA Equivalent | Why Semantic is Better |
|---|---|---|
<button>Click me</button> |
<div role="button" tabindex="0">Click me</div> |
Native keyboard support, focus management, form submission |
<a href="/page">Link</a> |
<span role="link" tabindex="0">Link</span> |
Native navigation, right-click context menu, link preview |
<input type="checkbox"> |
<div role="checkbox" aria-checked="false"> |
Native keyboard, form integration, state management |
<input type="radio"> |
<div role="radio" aria-checked="false"> |
Native arrow key navigation within group |
<input type="range"> |
<div role="slider" aria-valuenow="50"> |
Native keyboard, mobile touch support, built-in value handling |
<header></header> |
<div role="banner"></div> |
Automatic landmark recognition |
<nav></nav> |
<div role="navigation"></div> |
Automatic landmark recognition |
<main></main> |
<div role="main"></div> |
Automatic landmark recognition |
<footer></footer> |
<div role="contentinfo"></div> |
Automatic landmark recognition |
<h1>-<h6> |
<div role="heading" aria-level="1"> |
SEO benefits, document outline, default styling |
Rule of Thumb: If there's a native HTML element for your use case, use it. Only use ARIA when creating custom widgets that have no HTML equivalent (like tabs, tree views, or custom comboboxes).
Common Accessibility Patterns
Proven patterns for implementing accessible interactive components.
Skip Links
| Pattern | Implementation |
|---|---|
| Skip to main content | <a href="#main" class="skip-link">Skip to main content</a> |
Modal Dialog
| Requirement | Implementation |
|---|---|
| Dialog markup | <div role="dialog" aria-modal="true" aria-labelledby="title"> |
| Focus trap | Trap Tab key within dialog, Shift+Tab cycles backward |
| Close on Esc | Close dialog when Escape key is pressed |
| Initial focus | Move focus to first interactive element or close button |
| Return focus | Return focus to trigger element when closed |
| Background | Set aria-hidden="true" on background content |
Tabs
| Element | Implementation |
|---|---|
| Tab list container | <div role="tablist" aria-label="Settings"> |
| Individual tabs | <button role="tab" aria-selected="true" aria-controls="panel1"> |
| Tab panels | <div role="tabpanel" id="panel1" aria-labelledby="tab1"> |
| Keyboard navigation | Arrow keys to navigate tabs, Tab to move to panel, Home/End |
| State management | One tab aria-selected="true", others false. Show/hide panels |
Accordion
| Element | Implementation |
|---|---|
| Accordion button | <button aria-expanded="false" aria-controls="section1"> |
| Accordion panel | <div id="section1" role="region" aria-labelledby="btn1"> |
| Keyboard navigation | Space/Enter to toggle, optional arrow keys between headers |
Form Validation
| State | Implementation |
|---|---|
| Required field | <input required aria-required="true"> |
| Invalid field | <input aria-invalid="true" aria-describedby="error1"> |
| Error message | <span id="error1" role="alert">Email is required</span> |
| Success notification | <div role="status">Form submitted successfully</div> |
Live Notifications
| Type | Implementation |
|---|---|
| Error alert | <div role="alert">Connection lost</div> |
| Status update | <div role="status">5 new messages</div> |
| Loading state | <div aria-live="polite" aria-busy="true">Loading...</div> |
Testing & Validation
Tools and techniques for testing accessibility.
Browser Tools
| Tool | What It Tests | How to Access |
|---|---|---|
| Chrome DevTools Accessibility | ARIA attributes, accessibility tree, color contrast | DevTools → Elements → Accessibility pane |
| Firefox Accessibility Inspector | Accessibility tree, keyboard navigation, ARIA properties | DevTools → Accessibility tab |
| Safari Accessibility Inspector | Accessibility tree and properties | Develop → Show Web Inspector → Accessibility |
| Lighthouse | Automated accessibility audit | Chrome DevTools → Lighthouse → Accessibility |
Screen Readers
| Screen Reader | Platform | Cost | Testing Priority |
|---|---|---|---|
| NVDA | Windows | Free | High - Most popular on Windows |
| JAWS | Windows | Paid | Medium - Professional users |
| VoiceOver | macOS, iOS | Built-in | High - Built into Apple devices |
| TalkBack | Android | Built-in | Medium - Mobile testing |
| Narrator | Windows | Built-in | Low - Basic testing |
Automated Testing Tools
| Tool | Type | Use Case |
|---|---|---|
| axe DevTools | Browser extension | Quick testing in browser |
| WAVE | Browser extension | Visual feedback on errors |
| pa11y | Command line | Automated CI/CD testing |
| axe-core | JavaScript library | Integration into test suites |
| Accessibility Insights | Desktop app/extension | Comprehensive manual + auto testing |
Manual Testing Checklist
| Test | How to Test |
|---|---|
| Keyboard navigation | Navigate entire site using only Tab, Shift+Tab, Enter, Space, Arrows, Esc |
| Focus visibility | Ensure focus indicator is visible on all interactive elements |
| Skip links | Tab to skip link and verify it works |
| Screen reader | Navigate page with NVDA or VoiceOver, verify announcements |
| Color contrast | Check text meets 4.5:1 ratio (3:1 for large text) |
| Zoom to 200% | Zoom page to 200% and verify layout doesn't break |
| Images | Verify all images have appropriate alt text |
| Form labels | Verify all inputs have associated labels |
| Landmarks | Verify proper heading hierarchy and landmark regions |
| Dynamic content | Verify live regions announce changes appropriately |
Important: Automated tools catch only about 30-40% of accessibility issues. Manual testing with real screen readers and keyboard navigation is essential for comprehensive accessibility testing.
Common Mistakes & Gotchas
Redundant ARIA
Don't add ARIA roles that duplicate native semantics.
Bad: <button role="button">
Good: <button>
aria-hidden on Focusable Elements
Never hide focusable elements from screen readers.
Bad: <button aria-hidden="true">
Good: Use CSS display: none or remove from DOM
Missing Labels
Every interactive element needs an accessible name.
Bad: <button><img src="close.svg"></button>
Good: <button aria-label="Close"><img src="close.svg" alt=""></button>
Keyboard Traps
Users must be able to navigate away from every element.
Bad: Modal with no way to close via keyboard
Good: Close button + Esc key handler
Empty Links/Buttons
Links and buttons without text content confuse screen readers.
Bad: <a href="/profile"></a>
Good: <a href="/profile" aria-label="View profile"></a>
Conflicting Roles
Don't override semantics of interactive elements.
Bad: <button role="heading">
Good: Use correct element for each purpose
Missing tabindex for Custom Widgets
Custom interactive elements need keyboard focus.
Bad: <div role="button">
Good: <div role="button" tabindex="0">
Placeholder as Label
Placeholders disappear on focus and don't provide persistent labels.
Bad: <input placeholder="Email">
Good: <label>Email</label><input placeholder="[email protected]">
disabled vs aria-disabled
disabled removes from tab order, aria-disabled doesn't.
Note: Use disabled for true disabled state
Use aria-disabled when element needs to stay focusable
Decorative Images with alt Text
Purely decorative images should have empty alt attribute.
Bad: <img src="border.png" alt="border">
Good: <img src="border.png" alt="">
Too Many Live Regions
Excessive announcements overwhelm screen reader users.
Tip: Use aria-live sparingly, batch updates when possible
Invalid ARIA Relationships
Referenced IDs in ARIA attributes must exist in the DOM.
Bad: aria-labelledby="nonexistent"
Good: Ensure referenced elements have matching IDs
Best Practice: When in doubt, use semantic HTML and test with actual screen readers. The ARIA specification is complex—it's better to use no ARIA than to use it incorrectly.