Fix: React Aria Not Working — Components Not Rendering, ARIA Attributes Missing, or Styling Conflicts
Part of: React & Frontend Errors
Quick Answer
How to fix React Aria and React Aria Components issues — hooks vs components API, styling with Tailwind CSS, custom components, collections pattern, forms, and accessibility compliance.
The Problem
A React Aria component renders but has no visible styles:
import { Button } from 'react-aria-components';
function App() {
return <Button>Click me</Button>;
// Renders a plain unstyled button — no visual feedback
}Or a Select component doesn’t open its popover:
import { Select, SelectValue, Popover, ListBox, ListBoxItem } from 'react-aria-components';
<Select>
<SelectValue />
<Popover>
<ListBox>
<ListBoxItem>Option 1</ListBoxItem>
</ListBox>
</Popover>
</Select>
// Click does nothing — popover never appearsOr custom components lose keyboard navigation:
Tab key doesn't focus the component, arrow keys don't workWhy This Happens
React Aria is Adobe’s accessibility-first component library. It comes in two forms — hooks (react-aria) and pre-built components (react-aria-components).
The library’s philosophy is that accessibility behavior (focus management, ARIA wiring, keyboard handling, screen reader announcements) is a runtime concern that lives inside the component, and visual styling is yours to own. That separation is the source of every “looks broken” bug — the component is doing its job, but the markup is invisible because no CSS targets the states React Aria exposes. The same separation is also the source of every silent accessibility regression: if you replace a React Aria primitive with a plain <div> to fix a styling issue, you lose all the wiring and screen reader users get no signal that anything changed.
- React Aria Components are unstyled — like Radix, they provide behavior and ARIA attributes without CSS. Every visual aspect (colors, borders, padding) must be added by you. Without styles, components are functional but invisible.
- Components have required children —
SelectneedsButton,Popover,ListBox, andListBoxItemas children in the correct structure. Missing a required child breaks the interaction chain. - Render props provide state for styling — React Aria Components use render props to expose state like
isPressed,isFocused,isSelected. You use these to apply conditional styles. - The hooks API requires manual ARIA wiring — if using hooks (
useButton,useSelect), you must spread the returned props onto DOM elements. Missing a spread loses keyboard handling and ARIA attributes.
A second class of failure is environmental. Server-rendered apps using Next.js App Router or Remix can serialize useId() differently across server and client, producing mismatched aria-labelledby references that throw hydration warnings and break screen reader pairing. Strict mode double-invocation in development can also make useFocus look broken because focus rings appear and immediately disappear. Both look like React Aria bugs but are environment bugs you need to diagnose differently.
Fix 1: Basic Components with Tailwind CSS
npm install react-aria-components
# Or for hooks API: npm install react-aria react-stately// Button with render props
import { Button } from 'react-aria-components';
function StyledButton({ children, ...props }) {
return (
<Button
{...props}
className={({ isHovered, isPressed, isFocusVisible, isDisabled }) =>
`inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium transition-colors
${isDisabled ? 'opacity-50 cursor-not-allowed bg-gray-200 text-gray-500' :
isPressed ? 'bg-blue-700 text-white scale-95' :
isHovered ? 'bg-blue-600 text-white' :
'bg-blue-500 text-white'}
${isFocusVisible ? 'ring-2 ring-blue-400 ring-offset-2' : ''}`
}
>
{children}
</Button>
);
}
// TextField
import { TextField, Label, Input, FieldError, Text } from 'react-aria-components';
function StyledTextField({ label, description, errorMessage, ...props }) {
return (
<TextField {...props} className="flex flex-col gap-1">
<Label className="text-sm font-medium text-gray-700">{label}</Label>
<Input className={({ isFocused, isInvalid }) =>
`px-3 py-2 rounded-lg border outline-none transition-colors
${isInvalid ? 'border-red-500 bg-red-50' :
isFocused ? 'border-blue-500 ring-2 ring-blue-200' :
'border-gray-300'}`
} />
{description && <Text slot="description" className="text-xs text-gray-500">{description}</Text>}
<FieldError className="text-xs text-red-500">{errorMessage}</FieldError>
</TextField>
);
}Fix 2: Select / Dropdown
import {
Select, SelectValue, Button, Label, Popover, ListBox, ListBoxItem,
} from 'react-aria-components';
interface Option {
id: string;
name: string;
}
function StyledSelect({ label, options, ...props }: {
label: string;
options: Option[];
}) {
return (
<Select {...props} className="flex flex-col gap-1">
<Label className="text-sm font-medium text-gray-700">{label}</Label>
<Button className={({ isFocused, isOpen }) =>
`flex items-center justify-between px-3 py-2 rounded-lg border outline-none transition-colors
${isOpen ? 'border-blue-500 ring-2 ring-blue-200' :
isFocused ? 'border-blue-400' :
'border-gray-300'}
bg-white`
}>
<SelectValue className="truncate" />
<span aria-hidden="true">▾</span>
</Button>
<Popover className="w-[--trigger-width] bg-white rounded-lg shadow-xl border border-gray-200 overflow-hidden">
<ListBox className="p-1 max-h-60 overflow-y-auto outline-none">
{options.map(option => (
<ListBoxItem
key={option.id}
id={option.id}
textValue={option.name}
className={({ isFocused, isSelected }) =>
`px-3 py-2 rounded-md outline-none cursor-pointer
${isSelected ? 'bg-blue-500 text-white' :
isFocused ? 'bg-blue-50 text-blue-900' :
'text-gray-900'}`
}
>
{option.name}
</ListBoxItem>
))}
</ListBox>
</Popover>
</Select>
);
}
// Usage
<StyledSelect
label="Framework"
options={[
{ id: 'react', name: 'React' },
{ id: 'vue', name: 'Vue' },
{ id: 'svelte', name: 'Svelte' },
]}
onSelectionChange={(key) => console.log('Selected:', key)}
/>Fix 3: Dialog / Modal
import {
DialogTrigger, Button, Modal, ModalOverlay, Dialog, Heading,
} from 'react-aria-components';
function ConfirmDialog() {
return (
<DialogTrigger>
<Button className="px-4 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600">
Delete
</Button>
<ModalOverlay className={({ isEntering, isExiting }) =>
`fixed inset-0 z-50 flex items-center justify-center bg-black/50
${isEntering ? 'animate-in fade-in duration-200' : ''}
${isExiting ? 'animate-out fade-out duration-150' : ''}`
}>
<Modal className={({ isEntering, isExiting }) =>
`w-full max-w-md bg-white rounded-xl shadow-2xl p-6
${isEntering ? 'animate-in zoom-in-95 duration-200' : ''}
${isExiting ? 'animate-out zoom-out-95 duration-150' : ''}`
}>
<Dialog className="outline-none">
{({ close }) => (
<>
<Heading slot="title" className="text-lg font-bold mb-2">
Confirm Deletion
</Heading>
<p className="text-gray-600 mb-6">
Are you sure? This action cannot be undone.
</p>
<div className="flex gap-3 justify-end">
<Button
onPress={close}
className="px-4 py-2 rounded-lg bg-gray-100 hover:bg-gray-200"
>
Cancel
</Button>
<Button
onPress={() => { handleDelete(); close(); }}
className="px-4 py-2 rounded-lg bg-red-500 text-white hover:bg-red-600"
>
Delete
</Button>
</div>
</>
)}
</Dialog>
</Modal>
</ModalOverlay>
</DialogTrigger>
);
}Fix 4: Table with Sorting and Selection
import {
Table, TableHeader, Column, TableBody, Row, Cell, Checkbox,
} from 'react-aria-components';
function DataTable({ data }: { data: User[] }) {
return (
<Table
aria-label="Users"
selectionMode="multiple"
sortDescriptor={{ column: 'name', direction: 'ascending' }}
onSortChange={(descriptor) => console.log('Sort:', descriptor)}
className="w-full border-collapse"
>
<TableHeader>
<Column isRowHeader allowsSorting className="text-left p-3 border-b font-medium">
Name
</Column>
<Column allowsSorting className="text-left p-3 border-b font-medium">
Email
</Column>
<Column className="text-left p-3 border-b font-medium">
Role
</Column>
</TableHeader>
<TableBody>
{data.map(user => (
<Row
key={user.id}
id={user.id}
className={({ isSelected, isFocused }) =>
`${isSelected ? 'bg-blue-50' : isFocused ? 'bg-gray-50' : ''}
border-b transition-colors`
}
>
<Cell className="p-3">{user.name}</Cell>
<Cell className="p-3">{user.email}</Cell>
<Cell className="p-3">{user.role}</Cell>
</Row>
))}
</TableBody>
</Table>
);
}Fix 5: Hooks API (Maximum Control)
// When you need full control over the DOM structure
import { useButton } from 'react-aria';
import { useRef } from 'react';
function CustomButton({ onPress, children }) {
const ref = useRef<HTMLButtonElement>(null);
const { buttonProps, isPressed } = useButton({ onPress }, ref);
return (
<button
{...buttonProps} // Spreads onClick, onKeyDown, role, tabIndex, etc.
ref={ref}
className={`px-4 py-2 rounded ${isPressed ? 'bg-blue-700' : 'bg-blue-500'} text-white`}
>
{children}
</button>
);
}
// useComboBox — autocomplete input
import { useComboBox, useFilter } from 'react-aria';
import { useComboBoxState } from 'react-stately';
function AutoComplete({ items, label }) {
const { contains } = useFilter({ sensitivity: 'base' });
const state = useComboBoxState({ items, defaultFilter: contains });
const inputRef = useRef(null);
const listBoxRef = useRef(null);
const popoverRef = useRef(null);
const { inputProps, listBoxProps, labelProps } = useComboBox(
{ inputRef, listBoxRef, popoverRef, label },
state,
);
return (
<div>
<label {...labelProps}>{label}</label>
<input {...inputProps} ref={inputRef} className="border rounded px-3 py-2" />
{state.isOpen && (
<div ref={popoverRef} className="absolute bg-white shadow-lg rounded mt-1">
<ul {...listBoxProps} ref={listBoxRef}>
{[...state.collection].map(item => (
<li key={item.key} className="px-3 py-2 hover:bg-gray-100 cursor-pointer">
{item.rendered}
</li>
))}
</ul>
</div>
)}
</div>
);
}Fix 6: Form Validation
import { Form, TextField, Label, Input, FieldError, Button } from 'react-aria-components';
function ContactForm() {
return (
<Form
onSubmit={(e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget));
console.log(data);
}}
validationBehavior="native"
className="flex flex-col gap-4 max-w-md"
>
<TextField name="name" isRequired className="flex flex-col gap-1">
<Label className="text-sm font-medium">Name</Label>
<Input className="px-3 py-2 border rounded-lg" />
<FieldError className="text-xs text-red-500" />
</TextField>
<TextField name="email" type="email" isRequired className="flex flex-col gap-1">
<Label className="text-sm font-medium">Email</Label>
<Input className="px-3 py-2 border rounded-lg" />
<FieldError className="text-xs text-red-500" />
</TextField>
<Button type="submit" className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600">
Submit
</Button>
</Form>
);
}Production Incident: The Silent Accessibility Regression
An accessibility regression rarely shows up in the metrics you watch. A sighted QA pass clears the staging build. Lighthouse reports a perfect score because it audits the markup, not the interaction. Conversion looks normal. Two months later you receive a customer complaint, or worse, a legal notice referencing WCAG 2.2. By then, the regression is on every page that imports the modified component.
A real example: a team replaced <Select> with a custom popover to fix a Tailwind styling conflict. The new component used <button> plus a positioned <div>, and it visually worked. But it had no role="listbox", no aria-activedescendant, no aria-expanded state synced to open/close, and no roving tabindex. Screen reader users heard a button announce itself, then silence — the options were never read aloud. The team only learned about it when a corporate client ran their site through a JAWS audit during procurement.
The blast radius for this kind of regression is “every screen reader user who depends on the component,” not “every user.” That makes the population small enough to never appear in bug reports but large enough to cause compliance problems and lost enterprise deals. Web accessibility lawsuits in the US grew double-digits year over year through the early 2020s and most settle out of court — the cost is real even when the incident never surfaces publicly.
Three monitoring hooks catch this class of regression. First, run @axe-core/playwright against your component library and your built pages in CI on every PR — fail the build on new violations, not on totals. Second, snapshot the rendered ARIA tree (role + name + state) of each interactive component and diff it in CI; React Aria’s wiring is stable enough that diffs are signal. Third, keep a short manual NVDA/VoiceOver script (open modal, navigate combobox, submit form) and run it before each release. Automated tools catch about 30-40% of real issues — the rest need a human listening.
When a regression does land, roll back at the component level, not at the page level. Because React Aria centralizes behavior, the fix is usually a one-line revert in a single primitive, not a rewrite across the app.
Still Not Working?
Component renders but is invisible — React Aria Components ship with zero CSS. Add styles via className (string or function for state-based styles). The render prop pattern className={({ isHovered }) => ...} gives you access to component state for dynamic styling.
Select/Popover doesn’t open — check the component tree structure. Select needs Button (trigger), Popover (container), ListBox, and ListBoxItem in the correct hierarchy. Missing the Button child means there’s no trigger element to open the popover.
Keyboard navigation doesn’t work — don’t add custom onClick or onKeyDown handlers that call e.stopPropagation(). React Aria manages keyboard events internally. Use onPress instead of onClick, and onSelectionChange instead of custom click handlers on list items.
TypeScript errors with render props — the className function receives state props specific to each component. Button provides isHovered, isPressed, isFocusVisible, isDisabled. ListBoxItem provides isSelected, isFocused. Check the docs for each component’s available states.
Hydration mismatch on useId-generated ARIA attributes — React Aria uses useId() for aria-labelledby and aria-describedby linkage. If the server and client render different ID sequences (because of conditional rendering, dev-only providers, or third-party wrappers that mount only on the client), you get a hydration warning and screen readers see broken references. Wrap the root in <I18nProvider> and <RouterProvider> consistently on both sides, and avoid conditional client-only wrappers around React Aria components.
Focus ring appears then disappears immediately in dev — React 18 strict mode double-invokes effects in development, which can make isFocusVisible flicker. The bug is dev-only and harmless in production builds. Confirm by running npm run build && npm run start and retesting.
Tooltip or popover positions itself off-screen on first open — React Aria’s positioning runs after measurement. If the trigger is inside a transform-ed parent (CSS transform, scale, translate), the popover’s offset math is wrong because transform creates a new containing block. Move the popover to a portal or remove the transform from the ancestor.
For related component library issues, see Fix: Radix UI Not Working, Fix: shadcn/ui Not Working, Fix: Chakra UI Not Working, and Fix: Mantine Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: cmdk Not Working — Command Palette Not Opening, Items Not Filtering, or Keyboard Navigation Broken
How to fix cmdk command palette issues — Dialog setup, custom filtering, groups and separators, keyboard shortcuts, async search, nested pages, and integration with shadcn/ui and Tailwind.
Fix: Conform Not Working — Form Validation Not Triggering, Server Errors Missing, or Zod Schema Rejected
How to fix Conform form validation issues — useForm setup with Zod, server action integration, nested and array fields, file uploads, progressive enhancement, and Remix and Next.js usage.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: Lingui Not Working — Messages Not Extracted, Translations Missing, or Macro Errors
How to fix Lingui.js i18n issues — setup with React, message extraction, macro compilation, ICU format, lazy loading catalogs, and Next.js integration.