Design.dev design.dev

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.