Skip to content

Fix: React Aria Not Working — Components Not Rendering, ARIA Attributes Missing, or Styling Conflicts

FixDevs ·

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 appears

Or custom components lose keyboard navigation:

Tab key doesn't focus the component, arrow keys don't work

Why 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):

  • 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 childrenSelect needs Button, Popover, ListBox, and ListBoxItem as 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.

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>
  );
}

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.

For related component library issues, see Fix: Radix UI Not Working and Fix: shadcn/ui Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles