ARIA (Accessible Rich Internet Applications)
ARIA is a set of attributes that make web content and applications more accessible to people with disabilities, especially those using assistive technologies like screen readers. ARIA supplements HTML to bridge accessibility gaps in complex web applications.
Core Principles
Only use ARIA when necessary
html<!-- Bad: Unnecessary ARIA --> <button role="button">Click me</button> <!-- Good: Native HTML is better --> <button>Click me</button> <!-- Good: ARIA when needed --> <div role="button" tabindex="0">Custom button</div>
Don't change native semantics
html<!-- Bad: Conflicting role --> <h1 role="button">I'm confused</h1> <!-- Good: Use appropriate element --> <button>I'm a button</button>
Make interactive elements keyboard accessible
html<!-- Bad: Not keyboard accessible --> <div role="button" onclick="handleClick()">Click</div> <!-- Good: Keyboard accessible --> <div role="button" tabindex="0" onclick="handleClick()" onkeydown="if(event.key==='Enter'||event.key===' ')handleClick()"> Click </div>
ARIA Attributes
Roles
Define what an element is or does.
html<!-- Landmark roles --> <header role="banner">Site header</header> <nav role="navigation">Main navigation</nav> <main role="main">Main content</main> <aside role="complementary">Sidebar</aside> <footer role="contentinfo">Footer</footer> <!-- Widget roles --> <div role="button">Custom button</div> <div role="tab">Tab item</div> <div role="dialog">Modal dialog</div> <div role="alert">Error message</div> <div role="progressbar">Loading...</div> <!-- Document structure roles --> <div role="article">Article content</div> <div role="list"> <div role="listitem">Item 1</div> <div role="listitem">Item 2</div> </div>
States and Properties
aria-label
Provides an accessible name.
html<!-- Icon-only button --> <button aria-label="Close dialog"> <svg><!-- X icon --></svg> </button> <!-- Search input --> <input type="text" aria-label="Search products" placeholder="Search..."> <!-- Override visible text when needed --> <button aria-label="Add item to cart"> <span>Add to cart</span> <span class="price">$19.99</span> </button>
aria-labelledby
References another element as the label.
html<h2 id="dialog-title">Confirm Delete</h2> <div role="dialog" aria-labelledby="dialog-title"> Are you sure you want to delete this item? </div> <!-- Multiple labels --> <div id="billing-title">Billing Address</div> <div id="shipping-title">Shipping Address</div> <form aria-labelledby="billing-title shipping-title"> <!-- Form content --> </form>
aria-describedby
References additional description.
html<input type="email" aria-describedby="email-hint email-error" required > <p id="email-hint">We'll never share your email</p> <p id="email-error" class="error">Please enter a valid email</p>
aria-hidden
Hides content from assistive technologies.
html<!-- Decorative icons --> <span aria-hidden="true">🎉</span> <!-- Hidden duplicate content --> <div aria-hidden="true"> <!-- Visible but ignored by screen readers --> </div> <!-- Warning: Don't hide focusable elements --> <!-- Bad --> <button aria-hidden="true">Click me</button>
aria-live
Announces dynamic content changes.
html<!-- Polite: Wait for user to finish --> <div aria-live="polite"> Item added to cart </div> <!-- Assertive: Interrupt immediately --> <div aria-live="assertive" role="alert"> Error: Payment failed </div> <!-- Off: Don't announce (default) --> <div aria-live="off">Updates</div> <!-- Common pattern: Status messages --> <div role="status" aria-live="polite" aria-atomic="true"> 3 items remaining </div>
aria-expanded
Indicates whether an element is expanded or collapsed.
html<button aria-expanded="false" aria-controls="menu" onclick="toggleMenu()" > Menu </button> <nav id="menu" hidden> <!-- Menu items --> </nav> <script> function toggleMenu() { const button = document.querySelector('[aria-expanded]'); const menu = document.getElementById('menu'); const isExpanded = button.getAttribute('aria-expanded') === 'true'; button.setAttribute('aria-expanded', !isExpanded); menu.hidden = isExpanded; } </script>
aria-selected
Indicates selection state in tabs, lists, etc.
html<div role="tablist"> <button role="tab" aria-selected="true" aria-controls="panel-1"> Tab 1 </button> <button role="tab" aria-selected="false" aria-controls="panel-2"> Tab 2 </button> </div> <div role="tabpanel" id="panel-1">Panel 1 content</div> <div role="tabpanel" id="panel-2" hidden>Panel 2 content</div>
aria-disabled vs disabled
html<!-- Native disabled: Not focusable, not in tab order --> <button disabled>Cannot interact</button> <!-- aria-disabled: Still focusable, needs JS to prevent action --> <button aria-disabled="true" onclick="return false;"> Disabled but focusable </button>
aria-current
Indicates the current item in a set.
html<nav> <a href="/" aria-current="page">Home</a> <a href="/about">About</a> <a href="/contact">Contact</a> </nav> <!-- Other values --> <a href="#step-2" aria-current="step">Step 2</a> <a href="#2024-01-15" aria-current="date">January 15</a> <a href="#section-3" aria-current="location">Section 3</a>
Common Patterns
Modal Dialog
html<div role="dialog" aria-labelledby="dialog-title" aria-describedby="dialog-desc" aria-modal="true"> <h2 id="dialog-title">Confirm Action</h2> <p id="dialog-desc">Are you sure you want to continue?</p> <button onclick="confirm()">Confirm</button> <button onclick="cancel()">Cancel</button> </div> <script> // Trap focus within dialog // Close on Escape key // Return focus when closed </script>
Dropdown Menu
html<button aria-haspopup="true" aria-expanded="false" aria-controls="dropdown-menu" > Menu <span aria-hidden="true">▼</span> </button> <ul id="dropdown-menu" role="menu" hidden> <li role="menuitem"> <a href="/profile">Profile</a> </li> <li role="menuitem"> <a href="/settings">Settings</a> </li> <li role="menuitem"> <a href="/logout">Logout</a> </li> </ul>
Tabs
html<div role="tablist" aria-label="Account settings"> <button role="tab" aria-selected="true" aria-controls="profile-panel" id="profile-tab" > Profile </button> <button role="tab" aria-selected="false" aria-controls="security-panel" id="security-tab" tabindex="-1" > Security </button> </div> <div role="tabpanel" id="profile-panel" aria-labelledby="profile-tab"> Profile content </div> <div role="tabpanel" id="security-panel" aria-labelledby="security-tab" hidden> Security content </div>
Loading State
html<!-- Button with loading state --> <button aria-busy="true" aria-disabled="true"> <span aria-hidden="true">⏳</span> <span>Loading...</span> </button> <!-- Region with loading state --> <div role="region" aria-busy="true" aria-label="Product list"> Loading products... </div>
Form Validation
html<label for="email">Email</label> <input type="email" id="email" aria-required="true" aria-invalid="false" aria-describedby="email-error" > <span id="email-error" role="alert" aria-live="polite"> <!-- Error message appears here --> </span> <script> function validateEmail(input) { const isValid = input.validity.valid; input.setAttribute('aria-invalid', !isValid); const error = document.getElementById('email-error'); error.textContent = isValid ? '' : 'Please enter a valid email'; } </script>
Accordion
html<div class="accordion"> <h3> <button aria-expanded="false" aria-controls="section1" id="accordion1" > Section 1 </button> </h3> <div id="section1" role="region" aria-labelledby="accordion1" hidden> Content for section 1 </div> </div>
Progress Bar
html<!-- Determinate --> <div role="progressbar" aria-valuenow="75" aria-valuemin="0" aria-valuemax="100" aria-label="Upload progress"> <div style="width: 75%">75%</div> </div> <!-- Indeterminate --> <div role="progressbar" aria-label="Loading" aria-valuetext="Loading, please wait"> <div class="spinner"></div> </div>
React Examples
Accessible Button
jsxfunction IconButton({ icon, label, onClick }) { return ( <button onClick={onClick} aria-label={label} > <span aria-hidden="true">{icon}</span> </button> ); } // Usage <IconButton icon="🗑️" label="Delete item" onClick={handleDelete} />
Toggle Button
jsxfunction ToggleButton() { const [isPressed, setIsPressed] = useState(false); return ( <button role="button" aria-pressed={isPressed} onClick={() => setIsPressed(!isPressed)} > {isPressed ? 'On' : 'Off'} </button> ); }
Live Region
jsxfunction Notification({ message }) { return ( <div role="status" aria-live="polite" aria-atomic="true" > {message} </div> ); }
Testing ARIA
Manual Testing
- Use screen reader (NVDA, JAWS, VoiceOver)
- Test keyboard navigation
- Use browser DevTools accessibility tab
- Check contrast ratios
Automated Tools
bash# Install axe-core npm install -D @axe-core/cli # Run accessibility tests axe https://example.com # Install pa11y npm install -D pa11y # Run tests pa11y https://example.com
React Testing
javascriptimport { axe, toHaveNoViolations } from 'jest-axe'; expect.extend(toHaveNoViolations); test('should not have accessibility violations', async () => { const { container } = render(<MyComponent />); const results = await axe(container); expect(results).toHaveNoViolations(); });
Common Mistakes
html<!-- Bad: Redundant role --> <button role="button">Click</button> <!-- Good: Native button --> <button>Click</button> <!-- Bad: Missing keyboard support --> <div role="button" onclick="handleClick()">Click</div> <!-- Good: Full keyboard support --> <div role="button" tabindex="0" onclick="handleClick()" onkeydown="if(event.key==='Enter')handleClick()" > Click </div> <!-- Bad: aria-label on non-interactive --> <div aria-label="Important">Content</div> <!-- Good: Use visible text or aria-labelledby --> <div> <h2 id="title">Important</h2> <div aria-labelledby="title">Content</div> </div>
Best Practices
- Use native HTML elements when possible
- Test with actual screen readers
- Provide text alternatives for all non-text content
- Ensure keyboard accessibility
- Use ARIA sparingly and correctly
- Keep dynamic content announcements concise
- Test color contrast
- Provide skip links for navigation
- Use semantic HTML first, ARIA second