React Aria: Adobe's Open Source Library That Gives Your Component Library Accessibility for Free
React Spectrum is Adobe’s opinionated, accessibility-first open-source stack—comprising React Spectrum (styled components), React Aria (unstyled hooks), React Stately (cross-platform state), and Internationalized (i18n)—built to enforce design consistency at enterprise scale while enabling deep extensibility.
Stop writing aria-* attributes by hand. React Aria handles keyboard navigation, focus management, and screen reader semantics so you don't have to.
I've implemented a dropdown select component at least four times across different projects.
Not because each version was bad. Because each version handled accessibility slightly differently, and none of them were actually right. The first one worked fine with a mouse. The second one added keyboard navigation — arrow keys, Enter, Escape — but it broke on iOS VoiceOver. The third one fixed VoiceOver but introduced a focus trap bug that let users Tab their way behind the modal backdrop. By the fourth time, I had a folder of utility functions for managing focus, a custom hook for keyboard event handling, and a growing suspicion that I was reinventing a wheel that someone smarter than me had already solved.
Turns out, they had. It's called React Aria.
What React Aria actually is
React Aria is an open source library from Adobe, part of the larger react-spectrum project (15.1k stars, Apache-2.0 license). The tagline on their site is "craft world-class accessible components with custom styles," which is accurate but undersells the engineering involved.
The core idea: React Aria gives you components and hooks with zero styling but complete, production-tested behavior. Keyboard interactions, screen reader semantics, focus management, touch handling, internationalization — all of it is built in and follows the W3C WAI-ARIA Authoring Practices Guide. You supply the CSS. React Aria supplies everything else.
It covers 50+ components: Button, Select, ComboBox, Dialog, DatePicker, Table, Slider, DragAndDrop, and more. Each one has been tested against multiple screen readers — NVDA, JAWS, VoiceOver on macOS and iOS, TalkBack on Android — across multiple browsers.
The latest release as of this writing is React Aria Components v1.17.0 (April 2026), and it's actively maintained.
The problem it actually solves
Accessibility in UI components is harder than it looks. Here's what you're actually dealing with when you build a dropdown select from scratch:
Keyboard interactions: The component needs to respond to ArrowDown/ArrowUp for navigation, Enter to select, Escape to close, Home/End for first/last item, and typeahead (typing "A" should jump to the first item starting with A). None of this is automatic.
ARIA semantics: The container needs role="listbox", each option needs role="option", the selected option needs aria-selected="true", the trigger button needs aria-haspopup="listbox" and aria-expanded toggled appropriately, and the relationship between trigger and listbox needs to be wired with aria-controls. Get any of these wrong and screen readers announce the component incorrectly.
Focus management: When the dropdown opens, focus should move into the list. When it closes, focus should return to the trigger. If the user clicks outside, the dropdown should close without breaking focus flow.
Touch and mobile: Hover states don't work on touch devices. Long-press behavior needs to be handled differently. Touch scrolling inside the dropdown needs to be prevented from scrolling the background page.
Most developers implement two or three of these correctly and leave the rest. React Aria implements all of them.
import {
Select, Label, Button, SelectValue,
Popover, ListBox, ListBoxItem
} from 'react-aria-components'
function RoleSelect() {
return (
<Select>
<Label>Permissions</Label>
<Button>
<SelectValue />
<span aria-hidden>▼</span>
</Button>
<Popover>
<ListBox>
<ListBoxItem>Read Only</ListBoxItem>
<ListBoxItem>Edit</ListBoxItem>
<ListBoxItem>Admin</ListBoxItem>
</ListBox>
</Popover>
</Select>
)
}
// ✅ Arrow key navigation, typeahead, Escape to close
// ✅ Correct ARIA roles and attributes — no manual wiring
// ✅ Works with mouse, keyboard, and touchNo onKeyDown handlers. No aria-* attributes. No focus management code. The component reads correctly in every major screen reader out of the box.
Focus management: the detail that's easy to get wrong
Modal focus management is one of those problems that seems simple until you think about it carefully.
When a dialog opens, focus needs to move inside it. While the dialog is open, Tab and Shift+Tab need to cycle through focusable elements within the dialog — not escape into the background page. When the dialog closes, focus needs to return to wherever it was before the dialog opened (typically the button that triggered it).
Implementing this correctly requires keeping track of what had focus before the dialog opened, setting up a focus trap while the dialog is open, and cleaning it all up on close. It also needs to handle edge cases: what if the element that triggered the dialog no longer exists when the dialog closes? What if there are no focusable elements inside the dialog?
React Aria's Dialog handles all of this:
import { DialogTrigger, Button, Modal, Dialog, Heading } from 'react-aria-components'
function ConfirmDelete() {
return (
<DialogTrigger>
<Button>Delete item</Button>
<Modal>
<Dialog>
<Heading slot="title">Delete this item?</Heading>
<p>This action cannot be undone.</p>
<Button slot="close">Cancel</Button>
<Button>Confirm delete</Button>
</Dialog>
</Modal>
</DialogTrigger>
)
}
// ✅ Focus moves into the dialog on open
// ✅ Tab cycles within the dialog — doesn't escape to the background
// ✅ Focus returns to "Delete item" button on close
// ✅ Esc key closes the dialogThe slot="close" prop on the Cancel button wires it to close the dialog automatically. No useState, no onClose callback threading.
Styling: the data-* attribute pattern
React Aria components are completely unstyled, but they expose every interactive state through data-* attributes. This turns out to be a clean API for styling:
.my-button {
background: #3b82f6;
border-radius: 6px;
padding: 8px 16px;
color: white;
border: none;
cursor: pointer;
}
/* Style states with attribute selectors */
.my-button[data-pressed] {
background: #1d4ed8;
}
.my-button[data-hovered] {
background: #2563eb;
}
.my-button[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.my-button[data-focus-visible] {
outline: 2px solid #3b82f6;
outline-offset: 2px;
}The data-focus-visible attribute is worth calling out specifically. React Aria only applies it when the user is navigating with the keyboard — not when clicking with a mouse. This means you can show a clear focus ring for keyboard users without showing it to mouse users who don't need it. Getting this distinction right natively is surprisingly tricky (:focus-visible in CSS has inconsistent browser support for custom components). React Aria just handles it.
For Tailwind users, the official tailwindcss-react-aria-components plugin adds state variants:
<Button className="bg-blue-600 pressed:bg-blue-700 disabled:opacity-50 focus-visible:outline-2">
Submit
</Button>How it compares to Radix UI and Headless UI
If you've been in the headless component space for a while, you've probably used Radix UI or Headless UI. React Aria is in the same category, but there are real differences worth knowing.
Radix UI has excellent accessibility for the components it covers and a clean composition API. Its limitation is coverage: it doesn't include date/time pickers, drag and drop, or complex grid/table components. For most landing pages and marketing sites, Radix is probably sufficient. For data-heavy admin tools, it falls short.
Headless UI (from Tailwind Labs) is tightly integrated with Tailwind and has a minimal, easy-to-learn API. Coverage is also limited — around 10–12 components. It's a good choice if your entire stack is Tailwind-first and you want the simplest possible API for the common cases.
React Aria has the broadest coverage of the three, the most rigorous accessibility testing, and built-in internationalization that neither Radix nor Headless UI offer. The tradeoff is a steeper learning curve — the component API is more explicit and verbose than Radix's. If you're building an internal tool or design system that needs to handle complex interactions (multi-select tables, date ranges, drag-to-reorder lists), React Aria is the most complete option.
The layered API
One thing I appreciate about React Aria's design is that it has multiple levels of abstraction, and you can choose where to engage.
Level 1 — React Aria Components: The high-level API shown in this article. Pre-composed components with sensible defaults. Best for most use cases.
Level 2 — Contexts: You can reach into any component's context to build custom patterns or extend behavior. The library exports contexts for every component:
import { ButtonContext } from 'react-aria-components'
// Build a custom Stepper using Button's context
function Stepper({ children }) {
const [value, setValue] = useState(0)
return (
<ButtonContext.Provider value={{
slots: {
decrement: { onPress: () => setValue(v => v - 1) },
increment: { onPress: () => setValue(v => v + 1) },
}
}}>
{children}
</ButtonContext.Provider>
)
}Level 3 — Hooks: Full low-level control. useSelect, useButton, useCalendar, useDatePicker — every component has a corresponding hook. You control the DOM structure entirely. Useful when you need behavior that doesn't fit the component model.
This layered design means you can start simple and drop down to a lower level only when you need to, without switching libraries.
Internationalization out of the box
This is the feature that separates React Aria from most alternatives. It supports 30+ languages, 13 calendar systems (Gregorian, Japanese, Buddhist, Persian, Hebrew, Islamic variants, and more), 5 numbering systems, and right-to-left layout.
For a DatePicker, this means the component automatically formats dates according to the user's locale, handles calendars that don't count years the same way, and flips its layout direction for Arabic or Hebrew users. None of this requires any extra configuration on your part.
If you're building for a global audience, this is significant. It's an enormous amount of edge-case handling that would take months to implement correctly from scratch.
Who should use it
React Aria is the right tool if you're building a component library or design system with your own visual style, need serious accessibility support, and want to avoid building keyboard and focus behavior from scratch.
It's particularly compelling for internal tooling and admin dashboards — the kind of apps that get used by power users who navigate with keyboards and often include users with disabilities. In enterprise software, accessibility compliance (WCAG 2.1 AA or higher) is increasingly a procurement requirement, not just a nice-to-have.
It's less necessary if you just need a handful of standard components for a marketing site and your existing library (MUI, Ant Design, Chakra) already covers your use cases. In those scenarios, the unstyled headless approach adds overhead without enough benefit.
Getting started
Install the high-level components package:
npm install react-aria-componentsThe official documentation includes a getting-started guide that walks through installation and building a styled component from scratch. There are also example projects showing integration with Tailwind, Styled Components, and vanilla CSS.
If you want to explore before committing, the examples gallery shows fully styled, production-quality implementations — kanban boards, data tables, CRUD interfaces — all built on React Aria. It's a good way to see what the components look like after styling.
Accessibility used to be something teams added at the end of a project, if they added it at all. Libraries like React Aria are making it a default starting point. The behavior is correct from day one. You just decide how it looks.
If you found this useful, the React Aria documentation is thorough and worth bookmarking: react-aria.adobe.com. The GitHub repository is at github.com/adobe/react-spectrum.
