Introduction
ARIA (Accessible Rich Internet Applications) is a set of attributes that supplement HTML to communicate roles, states, and properties to assistive technologies. It bridges the gap when native HTML elements can't express the semantics of custom widgets.
But ARIA is frequently misused. The first rule of ARIA is: don't use ARIA if native HTML can do the job. A <button> is always better than <div role='button'>. ARIA doesn't add behavior — it only changes what screen readers announce.
This guide covers the most important ARIA attributes, when to use them, and common patterns for custom widgets.
Key Concepts
ARIA Roles
<!-- Landmark roles (prefer semantic HTML) -->
<nav>...</nav> <!-- Better than <div role="navigation"> -->
<main>...</main> <!-- Better than <div role="main"> -->
<div role="search">...</div> <!-- No HTML equivalent -->
<!-- Widget roles -->
<div role="tablist">
<button role="tab" aria-selected="true">Tab 1</button>
<button role="tab" aria-selected="false">Tab 2</button>
</div>
<div role="tabpanel">Content</div>
<!-- Live region roles -->
<div role="alert">Error message</div> <!-- Assertive announcement -->
<div role="status">3 results</div> <!-- Polite announcement -->
ARIA States and Properties
// States (change dynamically)
aria-expanded="true" // Dropdown open
aria-selected="true" // Tab or option selected
aria-checked="true" // Checkbox checked
aria-disabled="true" // Disabled (but still focusable)
aria-hidden="true" // Hidden from assistive technology
aria-busy="true" // Loading
aria-invalid="true" // Form validation error
aria-pressed="true" // Toggle button active
// Properties (relatively static)
aria-label="Close dialog" // Accessible name
aria-labelledby="heading-id" // Reference to labeling element
aria-describedby="help-text-id" // Reference to description
aria-controls="panel-id" // What this element controls
aria-live="polite" // Announce changes
aria-required="true" // Required form field
Practical Examples
1. Custom Dropdown
function Dropdown({ label, options, value, onChange }) {
const [open, setOpen] = useState(false);
return (
<div className="relative">
<button
aria-haspopup="listbox"
aria-expanded={open}
aria-label={label}
onClick={() => setOpen(!open)}
>
{value || 'Select...'}
</button>
{open && (
<ul role="listbox" aria-label={label}>
{options.map(opt => (
<li key={opt} role="option" aria-selected={opt === value}
onClick={() => { onChange(opt); setOpen(false); }}>
{opt}
</li>
))}
</ul>
)}
</div>
);
}
2. Accordion
function Accordion({ items }) {
const [openIndex, setOpenIndex] = useState<number | null>(null);
return (
<div>
{items.map((item, i) => (
<div key={i}>
<h3>
<button
aria-expanded={openIndex === i}
aria-controls={`panel-${i}`}
onClick={() => setOpenIndex(openIndex === i ? null : i)}
>
{item.title}
</button>
</h3>
<div id={`panel-${i}`} role="region" aria-labelledby={`heading-${i}`}
hidden={openIndex !== i}>
{item.content}
</div>
</div>
))}
</div>
);
}
3. Toast Notification
// Use aria-live for non-urgent updates
function ToastContainer({ toasts }) {
return (
<div aria-live="polite" aria-atomic="false" className="fixed bottom-4 right-4">
{toasts.map(toast => (
<div key={toast.id} role="status" className="rounded bg-white p-4 shadow">
{toast.message}
</div>
))}
</div>
);
}
// Use role="alert" for urgent errors
<div role="alert">Payment failed. Please try again.</div>
Best Practices
- ✅ Use native HTML elements first — <button>, <select>, <input> over ARIA widgets
- ✅ Always pair role with required states (tab needs aria-selected, checkbox needs aria-checked)
- ✅ Use aria-labelledby to reference visible text instead of duplicating with aria-label
- ✅ Use aria-describedby for supplementary information (help text, error messages)
- ✅ Test with a screen reader after adding ARIA — verify it announces correctly
- ❌ Don't use role='button' on a <div> — use <button> instead
- ❌ Don't use aria-hidden='true' on focusable elements — creates confusing ghost elements
Common Pitfalls
- Adding aria-label to elements with visible text — screen readers may ignore the visible text
- Using aria-hidden on a parent that contains focusable children
- Forgetting to update aria-expanded when a dropdown opens/closes
- Using aria-live='assertive' for non-urgent updates — interrupts the user's reading flow
Related Guides
- Screen Reader Testing — Verify ARIA announcements in practice
- Keyboard Accessibility Guide — ARIA widgets need keyboard support
- Accessible Forms Guide — ARIA for form validation and errors
- Accessible Navigation Patterns — ARIA for menus and tabs