The first rule of ARIA
"No ARIA is better than bad ARIA."
Before reaching for ARIA, ask:
- Can I use a native HTML element? Native elements have built-in accessibility.
- Can I style a native element? A styled
<button>beats a<div role="button">. - Do I really need this custom widget? Simpler is often better.
Use ARIA when HTML alone can't express the semantics you needβbut use it correctly.
Essential ARIA attributes
Labeling
| Attribute | Purpose | Example |
|---|---|---|
aria-label |
Provides accessible name when no visible label | <button aria-label="Close">Γ</button> |
aria-labelledby |
Points to element(s) that label this element | <div aria-labelledby="heading1"> |
aria-describedby |
Points to element with additional description | <input aria-describedby="hint1"> |
State & properties
| Attribute | Purpose | Values |
|---|---|---|
aria-expanded |
Indicates expandable element state | true / false |
aria-selected |
Indicates selection state | true / false |
aria-checked |
Checkbox/toggle state | true / false / mixed |
aria-pressed |
Toggle button state | true / false |
aria-hidden |
Hides from assistive tech (not visual) | true / false |
aria-live |
Announces dynamic content changes | polite / assertive / off |
Pattern: Button
First choice: Use <button> element.
If you must use a div/span:
<div role="button"
tabindex="0"
onclick="handleClick()"
onkeydown="handleKeyDown(event)">
Click me
</div>
<script>
function handleKeyDown(event) {
// Buttons respond to Enter and Space
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
handleClick();
}
}
</script>
Requirements checklist
- β
role="button" - β
tabindex="0"(keyboard focusable) - β Handle Enter and Space key
- β Visible focus indicator
- β Accessible name (text content or aria-label)
Pattern: Modal dialog
HTML structure
<div role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-desc">
<h2 id="dialog-title">Confirm deletion</h2>
<p id="dialog-desc">This action cannot be undone.</p>
<button>Delete</button>
<button>Cancel</button>
</div>
Keyboard behavior
- Tab β Cycle through focusable elements inside dialog only
- Escape β Close dialog
- Focus trapped inside while open
- Focus returns to trigger element when closed
JavaScript requirements
function openModal(modal, triggerButton) {
// 1. Show modal
modal.style.display = 'block';
// 2. Store trigger for later
modal.triggerElement = triggerButton;
// 3. Move focus to first focusable element
const firstFocusable = modal.querySelector('button, [href], input');
firstFocusable.focus();
// 4. Trap focus
modal.addEventListener('keydown', trapFocus);
}
function closeModal(modal) {
modal.style.display = 'none';
modal.triggerElement.focus(); // Return focus
}
function trapFocus(event) {
if (event.key !== 'Tab') return;
const focusables = modal.querySelectorAll('button, [href], input');
const first = focusables[0];
const last = focusables[focusables.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
}
Pattern: Tabs
HTML structure
<div class="tabs">
<div role="tablist" aria-label="Course sections">
<button role="tab"
id="tab-1"
aria-selected="true"
aria-controls="panel-1">
Overview
</button>
<button role="tab"
id="tab-2"
aria-selected="false"
aria-controls="panel-2"
tabindex="-1">
Schedule
</button>
<button role="tab"
id="tab-3"
aria-selected="false"
aria-controls="panel-3"
tabindex="-1">
Resources
</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
Overview content...
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
Schedule content...
</div>
<div role="tabpanel" id="panel-3" aria-labelledby="tab-3" hidden>
Resources content...
</div>
</div>
Keyboard behavior
| Key | Action |
|---|---|
| Tab | Move into tablist, then to panel |
| β β | Move between tabs |
| Home | First tab |
| End | Last tab |
Key implementation detail
Use roving tabindex: Only the selected tab has tabindex="0"; others have tabindex="-1". Arrow keys move focus and update tabindex.
Pattern: Accordion
HTML structure
<div class="accordion">
<h3>
<button aria-expanded="true" aria-controls="sect1">
Section 1
</button>
</h3>
<div id="sect1" role="region" aria-labelledby="sect1-btn">
Section 1 content...
</div>
<h3>
<button aria-expanded="false" aria-controls="sect2">
Section 2
</button>
</h3>
<div id="sect2" role="region" aria-labelledby="sect2-btn" hidden>
Section 2 content...
</div>
</div>
Key points
- Buttons inside headings (maintain heading structure)
aria-expandedreflects statearia-controlslinks to panelhiddenattribute hides collapsed panels
Pattern: Dropdown menu
HTML structure
<div class="dropdown">
<button aria-haspopup="true"
aria-expanded="false"
aria-controls="menu1">
Actions βΌ
</button>
<ul role="menu" id="menu1" hidden>
<li role="menuitem"><a href="#">Edit</a></li>
<li role="menuitem"><a href="#">Duplicate</a></li>
<li role="separator"></li>
<li role="menuitem"><a href="#">Delete</a></li>
</ul>
</div>
Keyboard behavior
| Key | Action |
|---|---|
| Enter / Space | Open menu, focus first item |
| β | Open menu or next item |
| β | Previous item |
| Escape | Close menu, focus trigger |
| Home | First item |
| End | Last item |
Pattern: Live regions
Announce dynamic content changes to screen reader users.
Types
<!-- Polite: Waits for pause in speech --> <div aria-live="polite"> 3 items in cart </div> <!-- Assertive: Interrupts immediately --> <div role="alert"> Error: Please fill in all required fields </div> <!-- Status: For status updates --> <div role="status"> Saving... </div>
Best practices
- Live region must exist in DOM before content changes
- Use
politefor most updates - Reserve
assertive/alertfor errors - Keep messages concise
- Don't overuse β too many announcements are overwhelming
Testing your ARIA
Browser DevTools
- Chrome: Elements β Accessibility tab shows computed name/role
- Firefox: Accessibility Inspector shows full a11y tree
Screen reader testing
Test these scenarios:
- Can user identify what the element is?
- Can user determine current state?
- Can user operate the widget?
- Are changes announced appropriately?
Common ARIA mistakes
| Mistake | Fix |
|---|---|
| Using role on wrong element | Check allowed child roles |
| Duplicate IDs for aria-labelledby | Ensure IDs are unique |
| aria-hidden on focusable element | Also set tabindex="-1" |
| Missing required ARIA attributes | Check role requirements |
Resources
- ARIA Authoring Practices Guide (APG) β Definitive patterns
- MDN ARIA documentation
- Accessibility Support β AT support for ARIA
- Web & Apps Hub