Introduction
ARIA — Accessible Rich Internet Applications — is a specification that extends HTML with a set of attributes that communicate information to assistive technologies when native HTML semantics are insufficient. Screen readers, switch access devices, voice control software, and other assistive technologies rely on this information to understand what an element is, what state it is in, and how the user can interact with it.
The most important thing to know about ARIA before reading this reference is the first rule of ARIA use:
Do not use ARIA if you can use a native HTML element or attribute instead.
A <button> element is already a button to a screen reader. Adding role="button" to a <div> creates something that looks like a button to assistive technology but lacks keyboard operability, focus management, and activation behaviour — all of which you then have to implement manually. Native HTML is always preferable.
ARIA fills the gap for things native HTML cannot express — dynamic state changes, complex interactive widgets, live region announcements, and relationships between elements that are not adjacent in the DOM. Used correctly, ARIA makes the difference between an application that works for everyone and one that works only for sighted mouse users. Used incorrectly, it actively makes things worse — a screen reader user confronted with incorrect ARIA information is worse off than one confronted with no ARIA at all.
This reference covers every ARIA attribute in the specification, organised by category, with usage examples, common mistakes, and the patterns that actually work in production.
How ARIA Works — The Accessibility Tree
ARIA attributes modify the accessibility tree — a parallel representation of the DOM that assistive technologies consume. The accessibility tree contains four things for each node:
- Role — what the element is (button, dialog, navigation, alert)
- Name — what it is called (the label a screen reader announces)
- State — its current condition (expanded, checked, disabled, invalid)
- Properties — additional descriptive information (description, required, owns)
ARIA attributes modify these four things. Nothing else. ARIA does not add behaviour, it does not add keyboard interaction, it does not make elements focusable. It only adds information to the accessibility tree.
<!-- What the browser exposes to assistive technology for this element -->
<button
aria-expanded="true"
aria-controls="menu-dropdown"
aria-label="Open navigation menu"
>
<!-- Accessibility tree node:
Role: button (from native <button> element)
Name: "Open navigation menu" (from aria-label)
State: expanded = true (from aria-expanded)
Props: controls = "menu-dropdown" (from aria-controls)
--></button>
Category One: Widget Roles
Roles define what an element is. These are the most commonly needed roles when native HTML elements cannot serve the purpose.
role=“button”
Use only when a non-button element must behave as a button and using <button> is genuinely not possible.
<!-- ✅ Prefer this always -->
<button type="button" onclick="handleClick()">Save changes</button>
<!-- ❌ Avoid this — you must add tabindex, keyboard handlers, and focus styles manually -->
<div
role="button"
tabindex="0"
onclick="handleClick()"
onkeydown="handleKey(event)"
>
Save changes
</div>
<!-- The only legitimate use: a framework component that renders a div
and cannot be changed to render a button -->
role=“checkbox”
<!-- Custom checkbox with correct ARIA -->
<div
role="checkbox"
aria-checked="true"
tabindex="0"
aria-labelledby="terms-label"
onkeydown="if(event.key===' '||event.key==='Enter') toggle(this)"
></div>
<span id="terms-label">I accept the terms and conditions</span>
<!-- Three states for indeterminate (e.g. select-all with partial selection) -->
<div role="checkbox" aria-checked="mixed" tabindex="0">Select all</div>
role=“radio” and role=“radiogroup”
<div role="radiogroup" aria-labelledby="priority-label">
<p id="priority-label">Ticket priority</p>
<div role="radio" aria-checked="false" tabindex="0">Low</div>
<div role="radio" aria-checked="true" tabindex="-1">Medium</div>
<div role="radio" aria-checked="false" tabindex="-1">High</div>
<!-- Note: only one tabindex="0" in the group — use roving tabindex -->
</div>
role=“switch”
A binary on/off toggle — semantically different from a checkbox.
<button
role="switch"
aria-checked="false"
onclick="this.setAttribute('aria-checked', this.getAttribute('aria-checked') === 'true' ? 'false' : 'true')"
>
Dark mode
</button>
<!-- Screen reader announces: "Dark mode, switch, off" -->
<!-- After toggle: "Dark mode, switch, on" -->
role=“slider”
<div
role="slider"
aria-valuenow="65"
aria-valuemin="0"
aria-valuemax="100"
aria-valuetext="65 percent"
aria-label="Volume"
tabindex="0"
></div>
role=“tab”, role=“tablist”, role=“tabpanel”
The complete tab pattern — one of the most commonly implemented custom widgets:
<div role="tablist" aria-label="Ticket management views">
<button
role="tab"
aria-selected="true"
aria-controls="panel-open"
id="tab-open"
tabindex="0"
>
Open
</button>
<button
role="tab"
aria-selected="false"
aria-controls="panel-resolved"
id="tab-resolved"
tabindex="-1"
>
Resolved
</button>
</div>
<div role="tabpanel" id="panel-open" aria-labelledby="tab-open" tabindex="0">
<!-- Open tickets content -->
</div>
<div
role="tabpanel"
id="panel-resolved"
aria-labelledby="tab-resolved"
tabindex="0"
hidden
>
<!-- Resolved tickets content -->
</div>
role=“dialog” and role=“alertdialog”
<!-- Modal dialog — focus must move inside when opened -->
<div
role="dialog"
aria-modal="true"
aria-labelledby="dialog-title"
aria-describedby="dialog-description"
>
<h2 id="dialog-title">Resolve ticket</h2>
<p id="dialog-description">
This action will mark the ticket as resolved and notify the account owner.
</p>
<!-- dialog content -->
<button>Confirm</button>
<button>Cancel</button>
</div>
<!-- Alert dialog — for actions requiring immediate confirmation -->
<!-- Screen reader announces the full content immediately when opened -->
<div
role="alertdialog"
aria-modal="true"
aria-labelledby="alert-title"
aria-describedby="alert-body"
>
<h2 id="alert-title">Delete terminal record</h2>
<p id="alert-body">This cannot be undone. Are you sure?</p>
<button>Delete permanently</button>
<button>Cancel</button>
</div>
role=“menu”, role=“menubar”, role=“menuitem”
<ul role="menu" aria-label="Terminal actions">
<li role="menuitem" tabindex="-1">View details</li>
<li role="menuitem" tabindex="-1">Assign engineer</li>
<li role="menuitem" tabindex="-1" aria-disabled="true">Reset terminal</li>
</ul>
<!-- menuitemcheckbox and menuitemradio for selectable menu items -->
<li role="menuitemcheckbox" aria-checked="true">Show offline only</li>
<li role="menuitemradio" aria-checked="false">Sort by region</li>
<li role="menuitemradio" aria-checked="true">Sort by status</li>
role=“listbox” and role=“option”
For custom select-like components:
<div
role="listbox"
aria-labelledby="status-filter-label"
aria-activedescendant="option-open"
>
<p id="status-filter-label">Filter by status</p>
<div role="option" id="option-all" aria-selected="false">All</div>
<div role="option" id="option-open" aria-selected="true">Open</div>
<div role="option" id="option-resolved" aria-selected="false">Resolved</div>
</div>
role=“combobox”
For search inputs with a dropdown suggestion list:
<div
role="combobox"
aria-expanded="true"
aria-haspopup="listbox"
aria-owns="suggestions-list"
>
<input
type="text"
aria-autocomplete="list"
aria-controls="suggestions-list"
aria-activedescendant="suggestion-1"
/>
</div>
<ul role="listbox" id="suggestions-list">
<li role="option" id="suggestion-1">FedEx Account #1042</li>
<li role="option" id="suggestion-2">FedEx Account #1043</li>
</ul>
role=“grid”, role=“row”, role=“gridcell”, role=“columnheader”, role=“rowheader”
For interactive data grids where cells are editable or selectable — distinct from a data table:
<div role="grid" aria-label="Terminal health grid" aria-rowcount="500">
<div role="row">
<div role="columnheader" aria-sort="ascending">Terminal ID</div>
<div role="columnheader" aria-sort="none">Status</div>
<div role="columnheader" aria-sort="none">Region</div>
</div>
<div role="row" aria-rowindex="1">
<div role="rowheader">ATM-001</div>
<div role="gridcell">Online</div>
<div role="gridcell">North</div>
</div>
</div>
role=“tree”, role=“treeitem”
For hierarchical navigation structures:
<ul role="tree" aria-label="File structure">
<li role="treeitem" aria-expanded="true" aria-level="1">
src
<ul role="group">
<li role="treeitem" aria-level="2" aria-selected="false">
<span>app.component.ts</span>
</li>
<li role="treeitem" aria-expanded="false" aria-level="2">
features
<ul role="group">
<li role="treeitem" aria-level="3" aria-selected="true">tickets</li>
</ul>
</li>
</ul>
</li>
</ul>
role=“tooltip”
<button aria-describedby="save-tooltip">Save</button>
<div role="tooltip" id="save-tooltip">
Save changes and notify assigned engineer
</div>
<!-- tooltip content is announced as a description, not a label -->
role=“progressbar”
<!-- Determinate progress — known completion percentage -->
<div
role="progressbar"
aria-valuenow="65"
aria-valuemin="0"
aria-valuemax="100"
aria-label="File upload progress"
aria-valuetext="65% complete"
></div>
<!-- Indeterminate progress — unknown completion time -->
<div
role="progressbar"
aria-label="Loading terminal data"
aria-valuetext="Loading..."
>
<!-- omit aria-valuenow when progress is indeterminate -->
</div>
role=“spinbutton”
For numeric inputs that increment/decrement:
<div
role="spinbutton"
aria-valuenow="5"
aria-valuemin="1"
aria-valuemax="100"
aria-label="Tickets per page"
tabindex="0"
>
5
</div>
role=“scrollbar”
<div
role="scrollbar"
aria-controls="content-area"
aria-valuenow="30"
aria-valuemin="0"
aria-valuemax="100"
aria-orientation="vertical"
tabindex="0"
></div>
role=“separator”
<!-- As a structural separator (no tabindex — purely presentational) -->
<div role="separator" aria-orientation="horizontal"></div>
<!-- As a focusable separator in a split panel (has tabindex — interactive) -->
<div
role="separator"
aria-orientation="vertical"
tabindex="0"
aria-valuenow="30"
aria-valuemin="10"
aria-valuemax="90"
aria-label="Resize panels"
></div>
Category Two: Landmark and Structure Roles
These define the regions of a page that users can jump between with screen reader shortcuts.
<!-- The full landmark structure of a well-organised page -->
<header role="banner">
<!-- page header — one per page -->
<nav role="navigation">
<!-- navigation — use aria-label if multiple navs -->
<main role="main">
<!-- main content — one per page -->
<aside role="complementary">
<!-- sidebar or supplementary content -->
<footer role="contentinfo">
<!-- page footer — one per page -->
<section role="region" aria-labelledby="section-heading">
<!-- named region -->
<form role="form" aria-labelledby="form-heading">
<!-- landmark form -->
<div role="search" aria-label="Search terminals">
<!-- search region -->
</div>
</form>
</section>
</footer>
</aside>
</main>
</nav>
</header>
Note: these landmark roles are already implied by the corresponding HTML elements (<header>, <nav>, <main>, etc.). Only add the role attribute explicitly when you cannot use the semantic HTML element.
Category Three: Live Region Attributes
Live regions announce dynamic content changes to screen readers without requiring the user to move focus. They are essential for any interface that updates asynchronously.
aria-live
The most important live region attribute. Controls when announcements are made.
<!-- aria-live="polite" — announces when the user is idle -->
<!-- Use for: status messages, search results, non-urgent updates -->
<div aria-live="polite" aria-atomic="true" id="status-message">
<!-- Updated dynamically: "Ticket resolved successfully" -->
</div>
<!-- aria-live="assertive" — interrupts immediately -->
<!-- Use for: errors, critical alerts, urgent system messages -->
<!-- Use sparingly — assertive interruptions are disruptive -->
<div aria-live="assertive" aria-atomic="true" id="error-message">
<!-- Updated dynamically: "Session expired. Please log in again." -->
</div>
<!-- aria-live="off" — no automatic announcements (default) -->
<div aria-live="off">
<!-- Changes are not announced -->
</div>
// Angular — announcing with LiveAnnouncer from @angular/cdk/a11y
import { LiveAnnouncer } from '@angular/cdk/a11y';
@Component({
/* ... */
})
export class TicketComponent {
constructor(private announcer: LiveAnnouncer) {}
async resolveTicket(id: string) {
await this.ticketService.resolve(id);
// Polite announcement — waits for user to be idle
this.announcer.announce('Ticket resolved successfully', 'polite');
}
async deleteTicket(id: string) {
await this.ticketService.delete(id);
// Assertive — interrupts immediately for critical feedback
this.announcer.announce('Ticket deleted', 'assertive');
}
}
aria-atomic
Controls whether the entire live region is announced or only the changed portion.
<!-- aria-atomic="true" — announces the entire region content when anything changes -->
<!-- Use when partial announcements would be confusing -->
<div aria-live="polite" aria-atomic="true">
<span>3 terminals offline</span>
<!-- announces the full sentence, not just "3" -->
</div>
<!-- aria-atomic="false" — announces only the changed nodes (default) -->
<div aria-live="polite" aria-atomic="false">
<span>Status: </span>
<span>Online</span>
<!-- only "Online" is announced when it changes -->
</div>
aria-relevant
Controls which types of changes trigger an announcement.
<!-- aria-relevant="additions" — only announce when nodes are added (default for most cases) -->
<div aria-live="polite" aria-relevant="additions">
<!-- New alert items appended here are announced -->
</div>
<!-- aria-relevant="removals" — announce when nodes are removed -->
<!-- Rare — screen readers do not usually need to hear what was removed -->
<!-- aria-relevant="text" — announce text content changes -->
<div aria-live="polite" aria-relevant="text additions">
<!-- Both text changes and new nodes are announced -->
</div>
<!-- aria-relevant="all" — announce any change -->
<!-- Use very sparingly — extremely verbose -->
aria-busy
Signals that a region is loading and announcements should be held until ready.
<!-- Set to true while loading, false when complete -->
<div aria-live="polite" aria-busy="true" id="results-region">
<!-- Loading... -->
</div>
<!-- When data arrives, set aria-busy="false" — then update content -->
<!-- Screen reader announces the complete content once, not incremental updates -->
<div aria-live="polite" aria-busy="false" id="results-region">
47 terminals found across 3 regions
</div>
Category Four: Relationship Attributes
These create explicit relationships between elements that may not be adjacent in the DOM.
aria-labelledby
Points to the element(s) that label this element. More powerful than aria-label — the label can be visible text anywhere in the document, and multiple IDs can be combined.
<!-- Basic labelledby -->
<h2 id="section-title">Open Tickets</h2>
<div role="region" aria-labelledby="section-title">
<!-- screen reader announces: "Open Tickets, region" -->
</div>
<!-- Combining multiple labels -->
<h2 id="account-name">Acme Corp</h2>
<h3 id="ticket-count">12 open tickets</h3>
<section aria-labelledby="account-name ticket-count">
<!-- Announced: "Acme Corp 12 open tickets, region" -->
</section>
<!-- Dialog labeled by its heading -->
<div role="dialog" aria-labelledby="modal-title" aria-modal="true">
<h2 id="modal-title">Assign ticket to engineer</h2>
<!-- content -->
</div>
aria-label
Provides an accessible name directly on the element — use when no visible label text exists.
<!-- Icon-only button — no visible text, must have aria-label -->
<button aria-label="Close dialog">
<svg aria-hidden="true" focusable="false">
<path d="M6 6 L18 18 M18 6 L6 18" />
<!-- X icon -->
</svg>
</button>
<!-- Multiple regions of the same type — distinguish with aria-label -->
<nav aria-label="Primary navigation">...</nav>
<nav aria-label="Breadcrumb navigation">...</nav>
<nav aria-label="Pagination">...</nav>
<!-- Search landmark -->
<div role="search" aria-label="Search terminals">
<input type="search" placeholder="Search by ID or location" />
</div>
<!-- ❌ Do not use aria-label and aria-labelledby together on the same element -->
<!-- aria-labelledby takes precedence and aria-label is ignored -->
aria-describedby
Points to element(s) that provide additional descriptive context — announced after the label and role.
<!-- Form field with hint and error -->
<label for="ticket-subject">Subject</label>
<input
type="text"
id="ticket-subject"
aria-describedby="subject-hint subject-error"
aria-invalid="true"
/>
<span id="subject-hint">Maximum 200 characters</span>
<span id="subject-error" role="alert">Subject is required</span>
<!-- Announced: "Subject, edit text, invalid. Maximum 200 characters. Subject is required." -->
<!-- Button with additional context -->
<button aria-describedby="delete-warning">Delete terminal record</button>
<p id="delete-warning" class="sr-only">
This permanently removes the terminal from all monitoring dashboards.
</p>
aria-controls
Identifies the element(s) that this element controls.
<!-- Accordion — button controls the panel -->
<button aria-expanded="false" aria-controls="faq-answer-1">
What is a terminal health alert?
</button>
<div id="faq-answer-1" hidden>A terminal health alert is triggered when...</div>
<!-- Tab controls its panel -->
<button
role="tab"
aria-selected="true"
aria-controls="panel-overview"
id="tab-overview"
>
Overview
</button>
<div role="tabpanel" id="panel-overview" aria-labelledby="tab-overview">
<!-- panel content -->
</div>
aria-owns
Declares ownership of elements that are not DOM children — useful when CSS positioning breaks the parent-child relationship.
<!-- Dropdown menu rendered in a portal at the body level
but logically owned by the button that opens it -->
<button aria-haspopup="true" aria-expanded="true" aria-owns="floating-menu">
Actions
</button>
<!-- Rendered at body level by a portal/overlay system -->
<ul id="floating-menu" role="menu">
<li role="menuitem">Edit</li>
<li role="menuitem">Delete</li>
</ul>
aria-activedescendant
Used in composite widgets (listbox, combobox, grid) to indicate which descendant is currently “active” without moving DOM focus. Focus stays on the container; aria-activedescendant points to the active item.
<input
type="text"
role="combobox"
aria-expanded="true"
aria-autocomplete="list"
aria-controls="search-suggestions"
aria-activedescendant="suggestion-2"
/>
<!-- focus remains on input -->
<ul id="search-suggestions" role="listbox">
<li role="option" id="suggestion-1">ATM-001</li>
<li role="option" id="suggestion-2" aria-selected="true">ATM-002</li>
<!-- aria-activedescendant points here — announced without moving focus -->
<li role="option" id="suggestion-3">ATM-003</li>
</ul>
aria-details
Points to an element with extended description — more detailed than aria-describedby. Used for complex content like figures, charts, or data tables where a full prose description is needed.
<img
src="terminal-status-chart.png"
alt="Terminal status distribution"
aria-details="chart-details"
/>
<div id="chart-details">
<p>This chart shows terminal status across all regions:</p>
<ul>
<li>North: 45 online, 3 offline</li>
<li>South: 38 online, 7 offline</li>
<li>East: 52 online, 1 offline</li>
</ul>
</div>
aria-flowto
Overrides the default reading order — allows you to specify a non-DOM-order sequence for screen reader navigation. Use very sparingly.
<!-- Reading order: intro → step-1 → step-2 → step-3 regardless of DOM order -->
<div id="intro" aria-flowto="step-1">Introduction content</div>
<div id="step-3">Step 3 content</div>
<!-- DOM order would read this second -->
<div id="step-1" aria-flowto="step-2">Step 1 content</div>
<div id="step-2" aria-flowto="step-3">Step 2 content</div>
Category Five: State Attributes
State attributes communicate the current condition of an element. They change dynamically as the user interacts with the application.
aria-expanded
Indicates whether a controlled element is expanded or collapsed.
<!-- Accordion -->
<button aria-expanded="false" aria-controls="answer-panel">
How do I assign a ticket?
</button>
<div id="answer-panel" hidden>...</div>
<!-- Navigation with submenu -->
<button aria-expanded="true" aria-haspopup="true">Account settings</button>
<!-- Combobox -->
<input aria-expanded="true" aria-haspopup="listbox" role="combobox" />
aria-selected
Indicates the selected state in a selection widget.
<!-- Tabs -->
<button role="tab" aria-selected="true">Open</button>
<button role="tab" aria-selected="false">Resolved</button>
<!-- Listbox options -->
<li role="option" aria-selected="true">High priority</li>
<li role="option" aria-selected="false">Medium priority</li>
<!-- Grid row selection -->
<tr role="row" aria-selected="true">
...
</tr>
aria-checked
The checked state for checkboxes, radio buttons, switches, and menu items.
<!-- Three states for checkboxes -->
<div role="checkbox" aria-checked="true">Selected</div>
<div role="checkbox" aria-checked="false">Unselected</div>
<div role="checkbox" aria-checked="mixed">Partially selected</div>
<!-- Switch -->
<button role="switch" aria-checked="false">Notifications</button>
<!-- Menu checkbox -->
<li role="menuitemcheckbox" aria-checked="true">Show alerts</li>
aria-pressed
For toggle buttons — different from aria-checked in that it applies to button elements, not form controls.
<!-- Toggle button — not a switch, not a checkbox, a button with a pressed state -->
<button
aria-pressed="false"
onclick="this.setAttribute('aria-pressed', this.getAttribute('aria-pressed') === 'true' ? 'false' : 'true')"
>
Mute notifications
</button>
<!-- Announced: "Mute notifications, toggle button, not pressed" -->
<!-- After click: "Mute notifications, toggle button, pressed" -->
aria-disabled
Communicates a disabled state to assistive technology. Note the difference from the HTML disabled attribute: aria-disabled="true" keeps the element in the tab order and focusable — the user can still reach it and hear why it is disabled. HTML disabled removes it from the tab order entirely.
<!-- HTML disabled — removed from tab order, screen reader may skip it -->
<button disabled>Submit (form incomplete)</button>
<!-- aria-disabled="true" — focusable, screen reader announces "dimmed" or "unavailable" -->
<!-- Use when you want users to know the button exists but cannot be activated yet -->
<button aria-disabled="true" onclick="return false">
Submit (complete all required fields to continue)
</button>
aria-hidden
Removes an element from the accessibility tree entirely. Everything inside is invisible to assistive technology.
<!-- Hide decorative icons from screen readers -->
<button>
<svg aria-hidden="true" focusable="false"><!-- icon --></svg>
Save changes
</button>
<!-- Hide decorative images -->
<img src="decorative-divider.svg" alt="" aria-hidden="true" />
<!-- Note: alt="" is correct for decorative images, aria-hidden redundant but harmless -->
<!-- Hide duplicate content -->
<span aria-hidden="true">★★★★☆</span>
<span class="sr-only">4 out of 5 stars</span>
<!-- ❌ Never use aria-hidden="true" on a focusable element -->
<!-- A keyboard user can still reach it but the screen reader announces nothing -->
<button aria-hidden="true">This is broken</button>
<!-- ❌ Never hide the focused element -->
<!-- Focus trap with aria-hidden creates an invisible keyboard hole -->
aria-invalid
Communicates validation state to assistive technology.
<!-- Invalid field -->
<input
type="email"
aria-invalid="true"
aria-describedby="email-error"
value="not-an-email"
/>
<span id="email-error" role="alert">Please enter a valid email address</span>
<!-- Grammar or spelling error (for rich text editors) -->
<span aria-invalid="grammar">their going to the meeting</span>
<span aria-invalid="spelling">teh annual report</span>
<!-- Explicitly valid — useful after successful validation -->
<input type="email" aria-invalid="false" value="user@example.com" />
aria-required
Indicates a field must be completed. Supplements the HTML required attribute for custom form controls.
<!-- HTML required — browsers show native validation UI -->
<input type="text" required aria-required="true" />
<!-- Note: aria-required is redundant here but harmless -->
<!-- Custom form control — no native required, must use aria-required -->
<div role="combobox" aria-required="true" aria-labelledby="status-label">
<!-- custom select-like widget -->
</div>
aria-current
Indicates the current item in a set — particularly useful for navigation.
<!-- Current page in navigation -->
<nav aria-label="Primary">
<a href="/dashboard">Dashboard</a>
<a href="/tickets" aria-current="page">Tickets</a>
<!-- currently active page -->
<a href="/monitoring">Monitoring</a>
</nav>
<!-- Current step in a wizard -->
<ol aria-label="Account setup steps">
<li><a href="#" aria-current="step">Profile</a></li>
<li><a href="#">Preferences</a></li>
<li><a href="#">Confirmation</a></li>
</ol>
<!-- Current date in a calendar -->
<td role="gridcell" aria-current="date">21</td>
<!-- Valid values: page, step, location, date, time, true -->
aria-grabbed and aria-dropeffect (deprecated)
These were part of the original drag-and-drop ARIA specification but have been deprecated. Use the HTML5 Drag and Drop API with appropriate event handling and live region announcements instead.
Category Six: Property Attributes
Properties provide additional descriptive information that does not change frequently with interaction.
aria-haspopup
Indicates that an element opens a popup — a listbox, tree, grid, dialog, or menu.
<!-- Opens a menu -->
<button aria-haspopup="menu" aria-expanded="false">Actions</button>
<!-- Opens a listbox (select-like dropdown) -->
<button aria-haspopup="listbox" aria-expanded="false">Filter by status</button>
<!-- Opens a dialog -->
<button aria-haspopup="dialog">Create ticket</button>
<!-- Opens a tree -->
<button aria-haspopup="tree" aria-expanded="false">Browse categories</button>
<!-- Opens a grid -->
<button aria-haspopup="grid" aria-expanded="false">Date picker</button>
<!-- aria-haspopup="true" is equivalent to "menu" — be specific when possible -->
aria-level
Defines the heading level in a tree or outline structure — use when the visual heading level needs to differ from the semantic level.
<!-- Custom heading hierarchy in a widget -->
<div role="heading" aria-level="3">Terminal Region: North</div>
<div role="heading" aria-level="4">Fault Summary</div>
<!-- In a tree -->
<li role="treeitem" aria-level="1" aria-expanded="true">Regions</li>
<li role="treeitem" aria-level="2">North</li>
<li role="treeitem" aria-level="3">ATM Terminals</li>
aria-orientation
Communicates whether a widget is oriented horizontally or vertically — affects keyboard navigation expectations.
<!-- Vertical menu — Up/Down arrow keys navigate -->
<ul role="menu" aria-orientation="vertical">
...
</ul>
<!-- Horizontal toolbar — Left/Right arrow keys navigate -->
<div role="toolbar" aria-orientation="horizontal">...</div>
<!-- Vertical slider -->
<div
role="slider"
aria-orientation="vertical"
aria-valuenow="50"
aria-valuemin="0"
aria-valuemax="100"
></div>
aria-valuemin, aria-valuemax, aria-valuenow, aria-valuetext
For range widgets — sliders, progress bars, spinbuttons, scrollbars.
<div
role="slider"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="65"
aria-valuetext="65% — 325 of 500 tickets resolved"
aria-label="Ticket resolution progress"
tabindex="0"
></div>
<!-- aria-valuetext overrides aria-valuenow in the announcement
Use it when the numeric value needs context to be meaningful -->
<!-- Without aria-valuetext: "65" -->
<!-- With aria-valuetext: "65% — 325 of 500 tickets resolved" -->
aria-multiselectable
Indicates that more than one item can be selected.
<div
role="listbox"
aria-multiselectable="true"
aria-label="Select regions to monitor"
>
<div role="option" aria-selected="true">North</div>
<div role="option" aria-selected="true">South</div>
<div role="option" aria-selected="false">East</div>
</div>
aria-multiline
Indicates whether a textbox accepts multiple lines.
<div
role="textbox"
aria-multiline="true"
aria-label="Ticket description"
contenteditable="true"
></div>
<!-- Tells screen reader: Enter key adds a new line, not submits -->
aria-placeholder
Provides hint text for empty inputs in custom form controls.
<!-- For native inputs, use the HTML placeholder attribute -->
<input type="text" placeholder="Search by terminal ID" />
<!-- For custom textbox roles, use aria-placeholder -->
<div
role="textbox"
aria-placeholder="Search by terminal ID"
aria-labelledby="search-label"
contenteditable="true"
></div>
aria-readonly
Indicates a value can be read but not modified.
<!-- HTML readonly — works for native inputs -->
<input type="text" readonly value="Account #1042" />
<!-- aria-readonly — for custom widgets -->
<div role="textbox" aria-readonly="true" aria-label="Account number">
Account #1042
</div>
<div role="spinbutton" aria-readonly="true" aria-valuenow="5">5</div>
aria-sort
Indicates the sort direction of a column header.
<table>
<tr>
<th aria-sort="ascending">Terminal ID</th>
<!-- sorted A→Z -->
<th aria-sort="descending">Last Updated</th>
<!-- sorted Z→A -->
<th aria-sort="none">Status</th>
<!-- sortable but not sorted -->
<th>Region</th>
<!-- not sortable — no aria-sort -->
</tr>
</table>
aria-keyshortcuts
Documents keyboard shortcuts associated with an element.
<button aria-keyshortcuts="Control+S">Save</button>
<button aria-keyshortcuts="Alt+N">New ticket</button>
<!-- Announced to screen reader users so they know shortcuts exist -->
aria-roledescription
Provides a human-readable description of a role to replace the default role announcement.
<!-- Changes "slide" role announcement to something more meaningful -->
<div
role="group"
aria-roledescription="slide"
aria-label="Terminal overview — slide 1 of 5"
>
<!-- carousel slide content -->
</div>
<!-- ❌ Do not use to rename semantic roles in misleading ways -->
<!-- ❌ <button aria-roledescription="link"> — confusing and incorrect -->
aria-colcount, aria-rowcount, aria-colindex, aria-rowindex, aria-colspan, aria-rowspan
For grids and tables where the full dataset is larger than what is rendered (virtual scrolling).
<!-- Grid with 500 rows but only 20 rendered at a time -->
<div role="grid" aria-rowcount="500" aria-colcount="6">
<div role="row" aria-rowindex="41">
<!-- currently showing rows 41-60 -->
<div role="gridcell" aria-colindex="1">ATM-041</div>
<div role="gridcell" aria-colindex="2">Online</div>
</div>
</div>
<!-- Merged cells in a grid -->
<div role="columnheader" aria-colspan="2" aria-colindex="3">Location</div>
aria-setsize and aria-posinset
For items in a set when the full set is not rendered (virtual scrolling, pagination).
<!-- Showing items 41-60 of 500 total -->
<li role="option" aria-setsize="500" aria-posinset="41">ATM-041</li>
<li role="option" aria-setsize="500" aria-posinset="42">ATM-042</li>
<!-- Screen reader announces: "ATM-041, 41 of 500" -->
aria-errormessage
Points to the element containing an error message — more specific than aria-describedby for validation errors.
<input type="email" aria-invalid="true" aria-errormessage="email-error-msg" />
<span id="email-error-msg" role="alert">
Please enter a valid email address
</span>
<!-- Note: aria-errormessage is only announced when aria-invalid is true -->
<!-- The referenced element should not use display:none — use visibility:hidden
or clip the element off-screen if you want it invisible but announced -->
aria-modal
Indicates that a dialog is modal — content outside should be treated as inert.
<div role="dialog" aria-modal="true" aria-labelledby="dialog-heading">
<h2 id="dialog-heading">Confirm resolution</h2>
<!-- With aria-modal="true", screen readers should not browse outside the dialog -->
<!-- Note: some screen readers require JavaScript focus trapping as well -->
</div>
Category Seven: Visually Hidden Content
Not technically an ARIA attribute, but the pattern used everywhere alongside ARIA — content that is visually hidden but present in the accessibility tree.
/* The correct visually-hidden class */
/* clip-path: inset(50%) is more reliable than clip: rect(0,0,0,0) in modern browsers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
/* Visible on focus — for skip links and focus-activated content */
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
padding: inherit;
margin: inherit;
overflow: visible;
clip-path: none;
white-space: normal;
}
<!-- Common uses of visually hidden content -->
<!-- Skip link — visible on focus, hidden otherwise -->
<a href="#main-content" class="sr-only sr-only-focusable"
>Skip to main content</a
>
<!-- Icon button label -->
<button>
<svg aria-hidden="true"><!-- close icon --></svg>
<span class="sr-only">Close dialog</span>
</button>
<!-- Additional context for screen readers -->
<a href="/tickets/1042">
View details
<span class="sr-only">for ticket #1042: Login broken</span>
</a>
<!-- Without sr-only: "View details" — ambiguous with multiple links -->
<!-- With sr-only: "View details for ticket #1042: Login broken" — clear -->
<!-- Star rating -->
<span aria-hidden="true">★★★★☆</span>
<span class="sr-only">Rated 4 out of 5 stars</span>
Quick Reference Table
| Attribute | Category | Purpose |
|---|---|---|
aria-label | Naming | Provides accessible name directly |
aria-labelledby | Naming | Points to element that provides the name |
aria-describedby | Description | Points to element with additional description |
aria-details | Description | Points to element with extended description |
aria-live | Live regions | Announces dynamic content changes |
aria-atomic | Live regions | Announce whole region or just changed part |
aria-relevant | Live regions | Which change types trigger announcement |
aria-busy | Live regions | Hold announcements while loading |
aria-expanded | State | Whether controlled content is expanded |
aria-selected | State | Selected state in selection widgets |
aria-checked | State | Checked state for checkboxes/switches |
aria-pressed | State | Pressed state for toggle buttons |
aria-disabled | State | Disabled but remains in tab order |
aria-hidden | State | Removes element from accessibility tree |
aria-invalid | State | Validation failure state |
aria-required | State | Field must be completed |
aria-current | State | Current item in a set |
aria-haspopup | Properties | Element opens a popup |
aria-controls | Relationship | Points to element this controls |
aria-owns | Relationship | Declares ownership of non-child elements |
aria-activedescendant | Relationship | Active item in composite widget |
aria-flowto | Relationship | Overrides reading order |
aria-sort | Properties | Column sort direction |
aria-orientation | Properties | Widget axis orientation |
aria-multiselectable | Properties | Multiple selection allowed |
aria-multiline | Properties | Textbox accepts multiple lines |
aria-placeholder | Properties | Hint text for empty custom inputs |
aria-readonly | Properties | Value readable but not editable |
aria-required | Properties | Input must have a value |
aria-level | Properties | Heading level in tree/outline |
aria-valuemin | Range | Minimum value |
aria-valuemax | Range | Maximum value |
aria-valuenow | Range | Current value |
aria-valuetext | Range | Human-readable value description |
aria-setsize | Set | Total items in set |
aria-posinset | Set | Position of item in set |
aria-rowcount | Grid | Total rows in grid |
aria-colcount | Grid | Total columns in grid |
aria-rowindex | Grid | Row position |
aria-colindex | Grid | Column position |
aria-rowspan | Grid | Rows spanned by cell |
aria-colspan | Grid | Columns spanned by cell |
aria-modal | Dialog | Content outside is inert |
aria-errormessage | Validation | Points to error message element |
aria-keyshortcuts | Properties | Documents keyboard shortcuts |
aria-roledescription | Properties | Custom role announcement text |
Conclusion
ARIA is a powerful tool and an easy one to misuse. The attributes in this reference exist because native HTML cannot express every interactive pattern that modern applications require — but native HTML should always be the first choice, and ARIA the fallback when it genuinely cannot do the job.
The most common mistake is using ARIA to describe what something looks like rather than what it is. A div with a coloured background is not a status indicator to a screen reader — it is nothing. An element with role="status" and content that describes the state is something a screen reader can work with.
The second most common mistake is applying ARIA attributes and then not testing them with an actual screen reader. Automated tools catch missing labels and incorrect nesting. They do not catch announcements that are technically correct but confusing in context, or live regions that fire at the wrong time, or focus management that works in the DOM but not in practice.
Use this reference to understand what is available. Use a screen reader to verify that what you built actually works.