DEV Community

Gaurav Guha
Gaurav Guha

Posted on Originally published at thefrontkit.com

Practical ARIA cheat sheet for React and Next.js

You're building a custom tab component or an icon-only button in React and the screen reader announces nothing useful. The fix is usually a handful of ARIA attributes, but you need to know which ones belong where.

Below is a concise reference that shows the exact JSX you can copy-paste into a Next.js or React project. It's organized by the UI pattern you're trying to solve, so you can jump straight to the code you need.

ARIA roles

Roles tell assistive technology what an element is. They override the element's native role, so use them only when there's no suitable HTML element.

Landmark roles

Landmarks let users jump between major sections. Prefer native tags (<header>, <nav>, <main>, etc.) because they already have implicit roles. Add an explicit role only when you can't use the semantic element.

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>
Enter fullscreen mode Exit fullscreen mode

Widget roles

When you build a component that has no native HTML counterpart, give it a role that matches its purpose.

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.

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>
Enter fullscreen mode Exit fullscreen mode

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>}
Enter fullscreen mode Exit fullscreen mode

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"
/>
Enter fullscreen mode Exit fullscreen mode

Labeling and describing

These attributes are the ones you'll reach for most often.

aria-label

Use when there is no visible label. Perfect for icon-only buttons or inputs that only have a placeholder.

// 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>
Enter fullscreen mode Exit fullscreen mode

aria-labelledby

Points to an element whose text is the label. Prefer this when a visible label already exists because it keeps the label consistent with what sighted users see.

// 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>
<span id="item-name">Project Alpha</span>
<!-- Screen reader announces: "Delete Project Alpha" -->
Enter fullscreen mode Exit fullscreen mode

aria-describedby

Provides additional description that follows the name. Use for help text, error messages, or any supplemental info.

// 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>
Enter fullscreen mode Exit fullscreen mode

aria-hidden

Removes an element from the accessibility tree while keeping it visible. Good for decorative icons or background images. Never hide interactive controls.

// 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>
Enter fullscreen mode Exit fullscreen mode

Relationship attributes you'll need

When two elements are related but not nested, connect them with the following attributes.

  • aria-controls: identifies the element that a button (or similar) controls.
  • aria-owns: tells assistive tech that an element logically owns another element that lives elsewhere in the DOM.
  • aria-describedby and aria-labelledby (covered above) also act as relationship hooks.

Gotcha: aria-controls does not automatically hide or show the target element. You still need to manage visibility with hidden, CSS, or React state. The attribute is only a hint for screen readers about the relationship.


That's the core set of ARIA attributes you'll use in a typical React/Next.js UI. Copy the snippets, adjust the IDs and state variables to match your component, and you'll have accessible widgets without pulling in a heavyweight library.


This is a shorter version of a guide I wrote on thefrontkit. The full post has the complete walkthrough.

Top comments (0)