ARIA Attributes Cheat Sheet for React Developers
accessibilityariareactnextjscheat-sheetreferencewcag8 min read

ARIA Attributes Cheat Sheet for React Developers

Admin

What Is ARIA?

ARIA (Accessible Rich Internet Applications) is a set of HTML attributes that tell assistive technologies like screen readers what a UI element is, what state it's in, and how it relates to other elements. When native HTML elements don't communicate enough information on their own, ARIA fills the gap.

The first rule of ARIA is: don't use ARIA if native HTML does the job. A <button> is better than <div role="button">. A <nav> is better than <div role="navigation">. Native elements come with built-in keyboard behavior, focus management, and screen reader announcements. ARIA adds none of these. It only changes what assistive technology announces.

That said, modern web apps are full of patterns that native HTML doesn't cover: tabs, accordions, dialogs, comboboxes, tree views, live notifications, and custom widgets. For these, ARIA is essential.

This cheat sheet covers the ARIA attributes you'll actually use in React and Next.js applications, organized by the pattern they solve.

ARIA Roles

Roles tell assistive technology what an element is. They override the element's native role.

Landmark Roles

Landmarks let screen reader users jump between major page sections. Use native HTML elements when possible (they have implicit roles), and add ARIA roles when you can't.

HTML Element Implicit Role When to Use ARIA Instead
<header> banner Only if not using semantic HTML
<nav> navigation Multiple navs need aria-label to distinguish
<main> main Only if not using semantic HTML
<aside> complementary Only if not using semantic HTML
<footer> contentinfo Only if not using semantic HTML
<section> region (if labeled) Must have aria-label or aria-labelledby
<form> form (if labeled) Must have aria-label or aria-labelledby
// Good: Native elements with labels for multiple navs
<nav aria-label="Main navigation">
  {/* Primary nav items */}
</nav>
<nav aria-label="Footer navigation">
  {/* Footer links */}
</nav>

// Good: Named section
<section aria-labelledby="settings-heading">
  <h2 id="settings-heading">Account Settings</h2>
  {/* Settings content */}
</section>

Widget Roles

For custom interactive components that don't have native HTML equivalents.

Role Purpose Common Usage
dialog Modal or non-modal dialog Modals, confirmation prompts, lightboxes
alertdialog Dialog that requires immediate attention Delete confirmation, unsaved changes warning
tablist / tab / tabpanel Tabbed interface Settings tabs, dashboard views
menu / menuitem Action menu Context menus, dropdown action menus
menubar / menuitem Horizontal menu bar Application menu bars
listbox / option Selectable list Custom select dropdowns, command palettes
combobox Text input with popup list Autocomplete, search suggestions
tree / treeitem Hierarchical list File browsers, nested navigation
grid / row / gridcell Interactive data grid Spreadsheet-like interfaces
toolbar Group of controls Text editor toolbars, formatting bars
tooltip Descriptive popup Hover/focus info popups
switch Binary toggle On/off toggles (distinct from checkbox)
status Live region for status updates Save confirmations, sync indicators
alert Urgent live region Error messages, warnings
progressbar Progress indicator File uploads, loading states

ARIA States and Properties

States describe the current condition of an element. Properties describe relationships between elements.

Toggle and Selection States

// Accordion: expanded/collapsed state
<button aria-expanded={isOpen} aria-controls="panel-1">
  Account Settings
</button>
<div id="panel-1" role="region" hidden={!isOpen}>
  {/* Panel content */}
</div>

// Checkbox: checked state
<div
  role="checkbox"
  aria-checked={isChecked}
  tabIndex={0}
  onClick={toggle}
  onKeyDown={(e) => e.key === ' ' && toggle()}
>
  {label}
</div>

// Switch/toggle: on/off
<button role="switch" aria-checked={isEnabled} onClick={toggle}>
  Dark mode: {isEnabled ? 'On' : 'Off'}
</button>

// Tab: selected state
<button role="tab" aria-selected={isActive} tabIndex={isActive ? 0 : -1}>
  General
</button>

// Menu item: checked (for toggle menu items)
<div role="menuitemcheckbox" aria-checked={isChecked}>
  Show notifications
</div>

// Radio-style selection
<div role="menuitemradio" aria-checked={isSelected}>
  Sort by name
</div>

// Tree item: expanded
<div role="treeitem" aria-expanded={isOpen} aria-level={2}>
  Components
</div>

Disabled and Busy States

// Disabled: element exists but is not interactive
<button aria-disabled={!canSubmit} onClick={canSubmit ? handleSubmit : undefined}>
  Submit
</button>
// Note: aria-disabled keeps the element in tab order (unlike HTML disabled)
// Use HTML disabled when you want to remove from tab order entirely:
<button disabled={!canSubmit}>Submit</button>

// Busy: element is loading or updating
<div aria-busy={isLoading} aria-live="polite">
  {isLoading ? <Spinner /> : content}
</div>

// Current: indicates current item in navigation
<a href="/dashboard" aria-current="page">Dashboard</a>
<a href="/settings">Settings</a>

// Invalid: form field has error
<input
  aria-invalid={hasError}
  aria-describedby={hasError ? 'email-error' : undefined}
/>
{hasError && <span id="email-error" role="alert">Enter a valid email address</span>}

Progress and Value States

// Progress bar
<div
  role="progressbar"
  aria-valuenow={65}
  aria-valuemin={0}
  aria-valuemax={100}
  aria-label="Upload progress"
>
  65%
</div>

// Indeterminate progress (no known completion)
<div role="progressbar" aria-label="Loading data">
  <Spinner />
</div>

// Slider
<input
  type="range"
  role="slider"
  aria-valuenow={volume}
  aria-valuemin={0}
  aria-valuemax={100}
  aria-label="Volume"
/>

Labeling and Describing

These are the attributes you'll use most often. They connect elements to their labels and descriptions.

aria-label

Provides a text label when there's no visible label. Use for icon buttons, landmark sections, and elements where the visible text doesn't fully describe the purpose.

// Icon button with no visible text
<button aria-label="Close dialog" onClick={onClose}>
  <XIcon aria-hidden="true" />
</button>

// Search input without visible label
<input type="search" aria-label="Search templates" placeholder="Search..." />

// Multiple navigation landmarks
<nav aria-label="Main">...</nav>
<nav aria-label="Breadcrumb">...</nav>

aria-labelledby

Points to another element whose text content serves as the label. Preferred over aria-label when a visible label exists because it uses the visible text, keeping the label consistent.

// Dialog labeled by its heading
<div role="dialog" aria-labelledby="dialog-title" aria-modal="true">
  <h2 id="dialog-title">Delete Account</h2>
  <p>This action cannot be undone.</p>
</div>

// Form section labeled by a heading
<section aria-labelledby="billing-heading">
  <h3 id="billing-heading">Billing Information</h3>
  <input type="text" aria-label="Card number" />
</section>

// Can reference multiple elements
<button aria-labelledby="action-label item-name">
  <span id="action-label">Delete</span>
</button>
{/* Where item-name is defined elsewhere: */}
<span id="item-name">Project Alpha</span>
{/* Screen reader announces: "Delete Project Alpha" */}

aria-describedby

Points to an element that provides additional description. Unlike aria-labelledby (which is the name), aria-describedby gives supplementary info read after the name.

// Input with help text
<label htmlFor="password">Password</label>
<input
  id="password"
  type="password"
  aria-describedby="password-help password-error"
/>
<span id="password-help">Must be at least 8 characters</span>
{error && <span id="password-error" role="alert">{error}</span>}

// Button with additional context
<button aria-describedby="delete-warning">Delete account</button>
<p id="delete-warning">This will permanently remove all your data.</p>

aria-hidden

Removes an element from the accessibility tree. The element is still visible but invisible to screen readers. Use for decorative elements, icons that have text labels, and duplicate content.

// Decorative icon next to text
<button>
  <TrashIcon aria-hidden="true" />
  Delete
</button>

// Decorative background image
<div aria-hidden="true" className="background-pattern" />

// NEVER hide interactive elements
// Bad: <button aria-hidden="true">Submit</button>

Relationship Attributes

These connect elements that are logically related but not parent-child in the DOM.

aria-controls

Identifies the element that this element controls. Used with buttons that show/hide content.

<button aria-controls="settings-panel" aria-expanded={isOpen}>
  Settings
</button>
<div id="settings-panel" hidden={!isOpen}>
  {/* Panel content */}
</div>

aria-owns

Indicates that an element logically owns elements that are not its DOM children. Use when DOM structure doesn't match logical structure (rare but necessary for portaled content like dropdowns rendered in a body-level container).

// Combobox with portaled listbox
<div role="combobox" aria-owns="suggestion-list" aria-expanded={isOpen}>
  <input aria-autocomplete="list" aria-activedescendant={activeId} />
</div>
{/* Rendered in a portal */}
<ul id="suggestion-list" role="listbox">
  <li role="option" id="option-1">React</li>
  <li role="option" id="option-2">Next.js</li>
</ul>

aria-activedescendant

Identifies the currently active child in a composite widget. Used in listboxes, menus, and trees where focus stays on the container and arrow keys move the active descendant.

<div
  role="listbox"
  tabIndex={0}
  aria-activedescendant={`option-${activeIndex}`}
  onKeyDown={handleArrowKeys}
>
  {options.map((option, i) => (
    <div
      key={option.id}
      id={`option-${i}`}
      role="option"
      aria-selected={i === activeIndex}
    >
      {option.label}
    </div>
  ))}
</div>

Live Regions

Live regions announce dynamic content changes to screen readers without moving focus. This is critical for notifications, loading states, error messages, and real-time updates.

aria-live

// Polite: Announced after the user finishes current task
<div aria-live="polite">
  {saveStatus === 'saved' && 'All changes saved'}
</div>

// Assertive: Announced immediately, interrupting current speech
<div aria-live="assertive">
  {connectionLost && 'Connection lost. Reconnecting...'}
</div>

role="status" (Polite Live Region)

Shorthand for aria-live="polite". Use for non-urgent status updates.

// Search results count
<div role="status">
  {results.length} results found
</div>

// Save indicator
<div role="status">
  {isSaving ? 'Saving...' : 'Saved'}
</div>

// Toast notification
<div role="status" className="toast">
  Settings updated successfully
</div>

role="alert" (Assertive Live Region)

Shorthand for aria-live="assertive". Use for urgent messages that require immediate attention.

// Form error
<div role="alert">
  {formError && `Error: ${formError}`}
</div>

// Session expiry warning
<div role="alert">
  Your session will expire in 2 minutes. Save your work.
</div>

Live Regions for AI Streaming

AI chat interfaces need live regions for streaming responses. The AI UX Kit handles this automatically, but here's the pattern:

// Streaming AI response
<div
  aria-live="polite"
  aria-atomic="false"
  aria-busy={isStreaming}
>
  {streamedContent}
</div>
// aria-atomic="false": Only announce new content, not the entire region
// aria-busy="true" during streaming: Tells screen readers to wait before announcing

For more on AI chat accessibility, see AI Chat UI Best Practices and Keyboard Navigation Patterns for Web Apps.

Complete Patterns: Copy-Paste JSX

Modal Dialog

<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="modal-title"
  aria-describedby="modal-description"
>
  <h2 id="modal-title">Confirm Deletion</h2>
  <p id="modal-description">
    This will permanently delete your account and all data.
  </p>
  <button onClick={onCancel}>Cancel</button>
  <button onClick={onConfirm}>Delete Account</button>
</div>

Tab Interface

<div role="tablist" aria-label="Settings sections">
  <button role="tab" aria-selected={true} aria-controls="panel-general" id="tab-general" tabIndex={0}>
    General
  </button>
  <button role="tab" aria-selected={false} aria-controls="panel-billing" id="tab-billing" tabIndex={-1}>
    Billing
  </button>
</div>
<div role="tabpanel" id="panel-general" aria-labelledby="tab-general" tabIndex={0}>
  {/* General settings content */}
</div>
<div role="tabpanel" id="panel-billing" aria-labelledby="tab-billing" tabIndex={0} hidden>
  {/* Billing settings content */}
</div>

Form with Errors

<form aria-label="Account settings" noValidate onSubmit={handleSubmit}>
  <div>
    <label htmlFor="email">Email address</label>
    <input
      id="email"
      type="email"
      aria-required="true"
      aria-invalid={!!errors.email}
      aria-describedby={errors.email ? 'email-error' : 'email-hint'}
    />
    <span id="email-hint">We'll send a confirmation to this address</span>
    {errors.email && (
      <span id="email-error" role="alert">{errors.email}</span>
    )}
  </div>

  <button type="submit" aria-disabled={isSubmitting}>
    {isSubmitting ? 'Saving...' : 'Save Changes'}
  </button>

  <div role="status" aria-live="polite">
    {saved && 'Changes saved successfully'}
  </div>
</form>

Data Table with Sorting

<table aria-label="Team members">
  <thead>
    <tr>
      <th scope="col" aria-sort={sortColumn === 'name' ? sortDirection : 'none'}>
        <button onClick={() => sort('name')}>
          Name
          {sortColumn === 'name' && (
            <span aria-hidden="true">{sortDirection === 'ascending' ? ' ▲' : ' ▼'}</span>
          )}
        </button>
      </th>
      <th scope="col" aria-sort={sortColumn === 'role' ? sortDirection : 'none'}>
        <button onClick={() => sort('role')}>Role</button>
      </th>
    </tr>
  </thead>
  <tbody>
    {rows.map(row => (
      <tr key={row.id}>
        <td>{row.name}</td>
        <td>{row.role}</td>
      </tr>
    ))}
  </tbody>
</table>
<div role="status" aria-live="polite">
  Sorted by {sortColumn}, {sortDirection}
</div>

Navigation with Current Page

<nav aria-label="Main navigation">
  <ul>
    <li><a href="/dashboard" aria-current={pathname === '/dashboard' ? 'page' : undefined}>Dashboard</a></li>
    <li><a href="/projects" aria-current={pathname === '/projects' ? 'page' : undefined}>Projects</a></li>
    <li><a href="/settings" aria-current={pathname === '/settings' ? 'page' : undefined}>Settings</a></li>
  </ul>
</nav>

Quick Reference Table

What You Need ARIA Attribute Example Value
Label an icon button aria-label "Close"
Point to a visible label aria-labelledby "heading-id"
Add help text to an input aria-describedby "hint-id"
Hide decorative element aria-hidden "true"
Show expanded/collapsed state aria-expanded true / false
Mark selected tab or option aria-selected true / false
Mark checked checkbox/switch aria-checked true / false / "mixed"
Show invalid form field aria-invalid true / false
Mark required field aria-required "true"
Disable without removing from tab order aria-disabled "true"
Connect trigger to content aria-controls "panel-id"
Announce status updates role="status" (on the container)
Announce urgent errors role="alert" (on the container)
Mark current page in nav aria-current "page"
Indicate sort direction aria-sort "ascending" / "descending" / "none"
Show loading state aria-busy true / false

Don't Reinvent It

Every pattern in this cheat sheet is already implemented in component libraries that handle ARIA correctly:

  • shadcn/ui + Radix UI: Used by the thefrontkit SaaS Starter Kit. All dialogs, menus, tabs, accordions, tooltips, and form components include correct ARIA attributes.
  • Headless UI: Accessible components from the Tailwind team.
  • React Aria (Adobe): Hooks for building custom accessible components.

The thefrontkit SaaS Starter Kit and AI UX Kit ship with every ARIA pattern in this guide already implemented and tested. Modals, dropdowns, tabs, data tables, forms, navigation, toast notifications, and AI streaming interfaces all include correct roles, states, properties, and live regions.

Related reading:

Start with accessible components: View SaaS Starter Kit | View AI UX Kit | Browse all templates

Related Posts