ARIA Implementation Guide
1. Core ARIA Roles Reference
| Role Category | Role Name | Purpose | Required Attributes |
|---|---|---|---|
| Widget Roles | button |
Clickable element triggering action | Accessible name (via label/text) |
| Widget Roles | checkbox |
Checkable input with 3 states | aria-checked (true/false/mixed) |
| Widget Roles | radio |
Single-select option in group | aria-checked, group context |
| Widget Roles | tab |
Tab in tab list for panels | aria-selected, aria-controls |
| Widget Roles | tablist |
Container for tabs | Contains tab roles |
| Widget Roles | tabpanel |
Content area controlled by tab | aria-labelledby (tab id) |
| Widget Roles | slider |
Range input control | aria-valuenow, aria-valuemin, aria-valuemax |
| Widget Roles | menuitem |
Option in menu | Parent menu or menubar |
| Widget Roles | menuitemcheckbox |
Checkable menu option | aria-checked |
| Widget Roles | menuitemradio |
Radio option in menu | aria-checked |
| Composite Roles | combobox |
Input with popup list | aria-expanded, aria-controls |
| Composite Roles | listbox |
List of selectable options | Contains option roles |
| Composite Roles | grid |
2D data table with interaction | row and gridcell children |
| Composite Roles | tree |
Hierarchical list with expand/collapse | treeitem children |
| Document Roles | article |
Independent content composition | Accessible name (recommended) |
| Document Roles | dialog |
Modal or non-modal dialog window | aria-labelledby or aria-label |
| Document Roles | alertdialog |
Dialog containing alert message | aria-labelledby, aria-describedby |
| Document Roles | tooltip |
Contextual popup information | Accessible name, referenced by aria-describedby |
| Live Region Roles | alert |
Important time-sensitive message | None (implicit aria-live="assertive") |
| Live Region Roles | status |
Advisory status message | None (implicit aria-live="polite") |
| Live Region Roles | log |
Sequential information log | None (live region behavior) |
| Live Region Roles | timer |
Numerical counter or timer | None (updates announced) |
Example: Common ARIA role implementations
<!-- Custom button -->
<div role="button" tabindex="0"
onclick="handleClick()" onkeydown="handleKeydown(event)">
Click Me
</div>
<!-- Checkbox -->
<div role="checkbox" aria-checked="false" tabindex="0">
Accept terms
</div>
<!-- Tab interface -->
<div role="tablist" aria-label="Account settings">
<button role="tab" aria-selected="true" aria-controls="panel1" id="tab1">
Profile
</button>
<button role="tab" aria-selected="false" aria-controls="panel2" id="tab2">
Security
</button>
</div>
<div role="tabpanel" id="panel1" aria-labelledby="tab1">
Profile content...
</div>
Warning: Use native HTML elements when available (
<button>,
<input>) instead of ARIA roles. First Rule of ARIA: Don't use
ARIA if you can use semantic HTML.
2. ARIA States and Properties Quick Guide
| Property/State | Values | Purpose | Common Usage |
|---|---|---|---|
| aria-label | String | Provides accessible name when none visible | aria-label="Close dialog" |
| aria-labelledby | ID reference(s) | References element(s) providing name | aria-labelledby="heading1" |
| aria-describedby | ID reference(s) | References element(s) providing description | aria-describedby="hint1" |
| aria-hidden | true/false | Hides element from accessibility tree | aria-hidden="true" for decorative icons |
| aria-expanded | true/false/undefined | Indicates if element is expanded | Accordion, dropdown, expandable sections |
| aria-checked | true/false/mixed | State of checkbox or radio | aria-checked="true" |
| aria-selected | true/false/undefined | Selection state in multi-selectable | Tabs, listbox options, tree items |
| aria-pressed | true/false/mixed | Toggle button pressed state | aria-pressed="true" for active state |
| aria-disabled | true/false | Indicates element is disabled | aria-disabled="true" (still focusable) |
| aria-required | true/false | Indicates required form field | aria-required="true" |
| aria-invalid | true/false/grammar/spelling | Validation error state | aria-invalid="true" with error message |
| aria-live | off/polite/assertive | Live region update priority | aria-live="polite" for status updates |
| aria-atomic | true/false | Announce entire region on change | aria-atomic="true" for timers |
| aria-relevant | additions/removals/text/all | Which changes to announce | aria-relevant="additions text" |
| aria-busy | true/false | Region currently updating | aria-busy="true" during load |
| aria-controls | ID reference(s) | Elements controlled by this element | aria-controls="panel1" |
| aria-owns | ID reference(s) | Defines parent-child relationship | When DOM structure doesn't reflect relationship |
| aria-current | page/step/location/date/time/true/false | Current item in set | aria-current="page" for current nav link |
| aria-haspopup | true/false/menu/listbox/tree/grid/dialog | Indicates popup type | aria-haspopup="menu" |
| aria-modal | true/false | Indicates modal dialog | aria-modal="true" for dialogs |
Example: ARIA states and properties in action
<!-- Expandable section -->
<button aria-expanded="false" aria-controls="details1">
Show details
</button>
<div id="details1" hidden>
Detailed content...
</div>
<!-- Form input with error -->
<input type="email"
id="email"
aria-required="true"
aria-invalid="true"
aria-describedby="email-error">
<span id="email-error" role="alert">
Please enter a valid email address
</span>
<!-- Loading state -->
<div aria-live="polite" aria-busy="true">
Loading content...
</div>
3. ARIA Live Regions and Announcements
| Live Region Type | aria-live Value | Priority | Use Case |
|---|---|---|---|
| role="alert" | assertive (implicit) | Immediate interruption | Errors, critical warnings, time-sensitive alerts |
| role="status" | polite (implicit) | Wait for pause in speech | Success messages, advisory info, progress updates |
| aria-live="polite" | polite | Non-interrupting announcement | Status updates, search results count, form hints |
| aria-live="assertive" | assertive | Interrupts current speech | Critical errors, timer expiration, system alerts |
| aria-live="off" | off | No announcements | Disabled live region (default) |
| role="log" | polite (implicit) | Sequential updates | Chat messages, activity feed, console output |
| role="timer" | off (implicit) | Periodic updates (use sparingly) | Countdown, stopwatch, live clock |
| Live Region Attribute | Values | Purpose |
|---|---|---|
| aria-atomic | true/false (default: false) | When true, announces entire region; when false, only changed nodes |
| aria-relevant | additions, removals, text, all | Which types of changes trigger announcements |
| aria-busy | true/false | Suppresses announcements during bulk updates |
Example: Live region implementations
<!-- Alert for errors -->
<div role="alert" aria-live="assertive">
Your session will expire in 1 minute
</div>
<!-- Status updates -->
<div role="status" aria-live="polite" aria-atomic="true">
<span id="search-count">24 results found</span>
</div>
<!-- Chat log -->
<div role="log" aria-live="polite" aria-relevant="additions">
<div>User1: Hello</div>
<div>User2: Hi there</div>
</div>
<!-- Loading with aria-busy -->
<div aria-live="polite" aria-busy="true">
<!-- Content being updated -->
</div>
<script>
// After update completes
element.setAttribute('aria-busy', 'false');
</script>
Note: Live regions must exist in DOM before content changes
occur. Create empty live region on page load, then update its content to trigger announcements.
Warning: Overuse of
aria-live="assertive" creates jarring experience. Reserve for
truly critical alerts only. Use polite for most cases.
4. ARIA Labeling Techniques
| Technique | When to Use | Priority | Example |
|---|---|---|---|
| Native Label | Form inputs with visible labels | 1st choice (best support) | <label for="name">Name:</label> |
| aria-labelledby | Multiple elements form the label | 2nd choice (flexible) | aria-labelledby="id1 id2 id3" |
| aria-label | No visible label text available | 3rd choice (translation issues) | aria-label="Close dialog" |
| aria-describedby | Additional context/instructions | Supplement to label | aria-describedby="hint1" |
| title attribute | Last resort (limited AT support) | Avoid if possible | title="Tooltip text" |
| Accessible Name Calculation | Understanding precedence | Knowledge essential | aria-labelledby > aria-label > native label > content |
Example: Various labeling techniques
<!-- Native label (preferred) -->
<label for="username">Username:</label>
<input type="text" id="username" name="username">
<!-- aria-labelledby (composite label) -->
<div id="billing-heading">Billing Address</div>
<div id="billing-hint">(must match credit card)</div>
<input type="text"
aria-labelledby="billing-heading billing-hint"
placeholder="123 Main St">
<!-- aria-label (icon button) -->
<button aria-label="Close dialog">
<svg aria-hidden="true"><!-- X icon --></svg>
</button>
<!-- aria-describedby (extra context) -->
<label for="password">Password:</label>
<input type="password"
id="password"
aria-describedby="password-requirements">
<div id="password-requirements">
Must be at least 8 characters with 1 number
</div>
<!-- Complex dialog labeling -->
<div role="dialog"
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>
<!-- Dialog content -->
</div>
Label Precedence Order
aria-labelledby(highest)aria-label<label>elementplaceholder(input only)- Element content text
titleattribute (lowest)
Common Mistakes:
- Using
aria-labelon<div>without role - Empty
aria-label=""(usearia-hidden) - Conflicting labels (both
aria-labelandlabelledby) - Referencing non-existent IDs
5. ARIA Best Practices and Patterns
| Best Practice | Guideline | Rationale |
|---|---|---|
| First Rule of ARIA | Use native HTML elements when possible | Native elements have built-in semantics, keyboard support, and better compatibility |
| Don't Change Native Semantics | Don't use role to override native HTML |
<button role="heading"> breaks expected behavior |
| Interactive ARIA Requires Keyboard | All interactive roles need keyboard handling | Add tabindex, Enter/Space handlers for custom controls |
| Don't Hide Focusable Elements | Never aria-hidden="true" on focusable elements |
Creates confusing experience - focus on invisible element |
| All Interactive Elements Are Focusable | Interactive elements need tabindex="0" or native focus |
Keyboard users must be able to reach and activate controls |
| Manage Focus Appropriately | Move focus to opened dialogs, restore on close | Keeps keyboard users oriented in document |
| Avoid Redundant ARIA | Don't add role="button" to <button> |
Native elements already have implicit roles |
| Update States Dynamically | Toggle aria-expanded, aria-checked with JS |
Screen readers announce state changes when attributes update |
| Provide Accessible Names | Every interactive element needs accessible name | Users must understand purpose of controls |
| Test With Screen Readers | Automated tools catch ~30% of issues | Real AT testing reveals actual user experience |
Example: Good vs bad ARIA usage
<!-- ❌ BAD: Overriding native semantics -->
<button role="heading">Click me</button>
<!-- ✅ GOOD: Use semantic HTML -->
<h2><button>Click me</button></h2>
<!-- ❌ BAD: aria-hidden on focusable element -->
<button aria-hidden="true" tabindex="0">Submit</button>
<!-- ✅ GOOD: Hide decorative, not interactive -->
<button>
<svg aria-hidden="true"><!-- icon --></svg>
Submit
</button>
<!-- ❌ BAD: Role without keyboard support -->
<div role="button" onclick="submit()">Submit</div>
<!-- ✅ GOOD: Proper keyboard handling -->
<div role="button"
tabindex="0"
onclick="submit()"
onkeydown="if(event.key==='Enter'||event.key===' ')submit()">
Submit
</div>
<!-- ✅ BEST: Use native button -->
<button onclick="submit()">Submit</button>
Warning: ARIA only changes semantics, not behavior. Adding
role="button" doesn't make an element clickable or keyboard accessible - you must implement those
features yourself.
6. ARIA Landmark Implementation
| Landmark Role | HTML Equivalent | Usage Guidelines | Labeling |
|---|---|---|---|
| role="banner" | <header> (page-level only) | Site-wide header at top of page (one per page) | Usually unlabeled; if multiple, use aria-label |
| role="navigation" | <nav> | Site/page navigation links (2-3 max recommended) | Required when multiple: aria-label |
| role="main" | <main> | Primary page content (exactly one per page) | Not needed (only one allowed) |
| role="complementary" | <aside> | Supporting content related to main content | Recommended: aria-label for clarity |
| role="contentinfo" | <footer> (page-level only) | Site footer with metadata (one per page) | Usually unlabeled |
| role="search" | None (must use role) | Search functionality container | Optional: aria-label="Site search" |
| role="region" | <section> with accessible name | Important content section (use sparingly) | Required: aria-labelledby or aria-label |
| role="form" | <form> with accessible name | Form becomes landmark only when labeled | Required for landmark: aria-label |
Example: Complete landmark structure
<!DOCTYPE html>
<html lang="en">
<body>
<!-- Banner landmark -->
<header>
<h1>Site Name</h1>
<!-- Navigation landmark -->
<nav aria-label="Primary navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<!-- Search landmark -->
<div role="search">
<form role="search" aria-label="Site search">
<input type="search" aria-label="Search query">
<button type="submit">Search</button>
</form>
</div>
</header>
<!-- Breadcrumb navigation -->
<nav aria-label="Breadcrumb">
<ol>
<li><a href="/">Home</a></li>
<li>Current Page</li>
</ol>
</nav>
<!-- Main landmark -->
<main id="main-content">
<h1>Page Title</h1>
<!-- Important region -->
<section aria-labelledby="updates-heading">
<h2 id="updates-heading">Latest Updates</h2>
<!-- Content -->
</section>
<!-- Form landmark -->
<form aria-label="Contact form">
<!-- Form fields -->
</form>
</main>
<!-- Complementary landmark -->
<aside aria-label="Related articles">
<h2>You might also like</h2>
<!-- Sidebar content -->
</aside>
<!-- Contentinfo landmark -->
<footer>
<p>© 2025 Company Name</p>
<nav aria-label="Footer navigation">
<!-- Footer links -->
</nav>
</footer>
</body>
</html>
Note: Screen reader users can navigate directly to landmarks with keyboard shortcuts (e.g.,
D for landmark list in NVDA). Proper landmark structure is critical for efficient
navigation.
Section 2 Key Takeaways
- First Rule of ARIA: Use native HTML elements instead of ARIA roles whenever possible
- ARIA only changes semantics, not behavior - you must add keyboard support manually
- Create live regions before updating their content to ensure announcements work
- Use
aria-live="polite"for most updates; reserve"assertive"for critical alerts only - Label all interactive elements with
aria-label,aria-labelledby, or native labels - Never use
aria-hidden="true"on focusable elements - Limit landmarks to 2-3 of each type maximum to avoid overwhelming users
- Test with actual screen readers - automated tools only catch ~30% of accessibility issues