# React Aria

You are working in a React 19 project that builds accessible components with React Aria Components (`react-aria-components` 1.x) and TypeScript, styled with Tailwind CSS v4. Write code for the version in `package.json`, not the one in your training data: several APIs you remember are deprecated in the installed version.

## Before you write code

- Read the types for the component you use: `node_modules/react-aria-components/dist/types/src/<Component>.d.ts`. Each render prop documents its data attribute (`@selector [data-pressed]`) and `@deprecated` marks what to avoid.
- Check `components/ui/` (or the project's equivalent) for wrappers and extend them instead of importing primitives into pages.
- Check the global CSS for `@plugin "tailwindcss-react-aria-components"`. If it is there, use its shorthand variants as the existing code does; otherwise use the `data-*` variants below.
- The package imports `client-only`: files that use it are Client Components (`"use client"` at the top in Next.js), and Server Components pass data to them as props.

## Components and props

- Import components from `react-aria-components`. Props use React Aria's names: `onPress`, `isDisabled`, `isSelected`, `isOpen`/`onOpenChange`, `isRequired`, `isInvalid`, `isReadOnly`. `onClick` on Button is only a discouraged alias: use `onPress`, which behaves the same for mouse, touch, keyboard and screen readers.
- Compose parts inside their parent and let context wire them: `Label`, `Input`, `Text slot="description"` and `FieldError` inside `TextField`; a `Button` and a `Popover` inside `Select`, `ComboBox`, `MenuTrigger` or `DialogTrigger`. Do not add `id`, `htmlFor` or `aria-labelledby` by hand.
- Use `isPending` on Button for loading: it keeps focus and announces progress. Buttons are `type="button"` unless you say `type="submit"`.
- In 1.22 these are deprecated: `Checkbox`, `Radio` and `Switch` (use `CheckboxField` with `CheckboxButton`, `RadioField` with `RadioButton`, `SwitchField` with `SwitchButton`); `Section` (use `ListBoxSection`, `MenuSection` and the other named sections); `selectedKey`, `defaultSelectedKey` and `onSelectionChange` on Select and ComboBox (use `value`, `defaultValue` and `onChange`, with `selectionMode="multiple"` for several); `selectedItem` in SelectValue's render props (use `selectedItems`); `validationState` (use `isInvalid`). Keep existing code working, but write new code with the current names.
- Check the exports before building your own: Autocomplete, Tree, Table, GridList, TagGroup, DropZone, Disclosure, Toolbar and the date and color pickers already exist. Toast is still `UNSTABLE_`-prefixed.

## Styling with render props and data attributes

- `className`, `style` and `children` accept a value or a function of render props: `className={({ isSelected }) => …}`, `{({ isOpen }) => …}`.
- Each state is also a data attribute, so Tailwind v4 variants work without functions: `data-hovered:bg-accent data-pressed:scale-[0.98] data-focus-visible:outline-2 data-selected:bg-primary data-disabled:opacity-50 data-invalid:border-destructive`. Others: `data-focused`, `data-open`, `data-expanded`, `data-indeterminate`, `data-placeholder`, `data-empty`, `data-entering`, `data-exiting`, `data-placement`.
- Prefer `data-hovered` and `data-focus-visible` to `hover:` and `focus:`: React Aria ignores emulated hover on touch and shows focus rings for keyboard users only. Style children with `group` and `group-data-selected:`.
- A string `className` replaces the default `react-aria-*` class. In wrappers, keep the caller's function working:

```tsx
import { Button as AriaButton, type ButtonProps, composeRenderProps } from "react-aria-components";
import { cn } from "@/lib/utils";

export function Button({ className, ...props }: ButtonProps) {
  return (
    <AriaButton
      {...props}
      className={composeRenderProps(className, (cls) =>
        cn("inline-flex h-9 items-center gap-2 rounded-md px-4 data-focus-visible:outline-2", cls),
      )}
    />
  );
}
```

- Popovers set `--trigger-width` (use `w-(--trigger-width)` for Select and ComboBox lists) and `--trigger-anchor-point` (use `origin-(--trigger-anchor-point)`). Animate with `data-entering:` and `data-exiting:` transitions or keyframes: React Aria keeps the overlay mounted until the exit animation ends.
- To render a router link or a Motion element, use `render={(props, renderProps) => <motion.button {...props} />}`. Render the same element type and spread `props`: they include the ref.

## Collections

- Collection components (ListBox, Menu, Select, ComboBox, GridList, Table, Tree, TagGroup) take static children or `items` with a render function:

```tsx
<ListBox aria-label="Members" items={members} selectionMode="multiple">
  {(member) => (
    <ListBoxItem id={member.id} textValue={member.name}>
      <img src={member.avatar} alt="" className="size-6 rounded-full" />
      {member.name}
    </ListBoxItem>
  )}
</ListBox>
```

- Every item needs a unique key: an `id` on the item element, or an `id` field on each object. Give items `textValue` when their children are not plain text, for typeahead and announcements.
- Sections are `ListBoxSection` or `MenuSection` with a `Header`; render a section's own items with `<Collection items={section.children}>`.
- Selection is `selectionMode`, `selectedKeys` and `onSelectionChange(keys)`, where `keys` is a `Set` or `"all"`; `disabledKeys` disables items without removing them. Pass `dependencies={[…]}` when a render function reads state from outside `items`.
- `Menu` handles actions with `onAction(key)`; submenus are `SubmenuTrigger` wrapping a `MenuItem` and a `Popover` with a `Menu`. Use `renderEmptyState` for empty lists.
- Use `useListData` for lists the user edits, `useAsyncList` for loading, paging and sorting, and `useFilter({ sensitivity: "base" })` for locale-aware matching.

## Overlays

- Modal: `DialogTrigger` > `Button` + `ModalOverlay` (backdrop, `isDismissable` to close on outside press) > `Modal` > `Dialog`. Give every Dialog a `<Heading slot="title">`, or an `aria-label`. `Dialog` children can be a function receiving `close`.
- Popover: `DialogTrigger` > `Button` + `Popover` (`placement="bottom start"`, `offset`, `OverlayArrow`) > `Dialog`. Menus, selects and combo boxes bring their own Popover child.
- Tooltip: `TooltipTrigger` > a focusable trigger + `Tooltip`. Tooltips show on hover and keyboard focus only; they never replace an accessible name, so icon-only buttons still get `aria-label`.
- Never add `z-index` stacks or portal code: overlays render in a portal and manage focus, scroll lock and Escape themselves.

## Forms

```tsx
<Form validationErrors={serverErrors} onSubmit={onSubmit}>
  <TextField name="email" type="email" isRequired>
    <Label>Email</Label>
    <Input />
    <Text slot="description">We send the receipt here.</Text>
    <FieldError />
  </TextField>
  <Button type="submit">Save</Button>
</Form>
```

- `validationBehavior` defaults to `"native"`: native constraints (`isRequired`, `minLength`, `pattern`, `type`) and `validate` block submission and show errors after a submit attempt. Use `"aria"` (on the field or the Form) for live validation that does not block submit.
- `validate={(value) => …}` returns an error string, an array of them, or `null`. For a schema, run its `safeParse` in `validate` and return the issue messages.
- Server errors go in `<Form validationErrors={{ email: "Taken" }}>`, keyed by field `name`; they clear when the user edits the field.
- With a form library, pass `isInvalid` and render the library's message as `FieldError` children; give each control `name`, `value` and `onChange` from the library.

## Links, locale and direction

- For client-side navigation, wrap the app in `RouterProvider` once, in a Client Component, so every `href` on Link, MenuItem, ListBoxItem, Tab or Row navigates with your router:

```tsx
"use client";
import { useRouter } from "next/navigation";
import { RouterProvider } from "react-aria-components";

declare module "react-aria-components" {
  interface RouterConfig {
    routerOptions: NonNullable<Parameters<ReturnType<typeof useRouter>["push"]>[1]>;
  }
}

export function Providers({ children }: { children: React.ReactNode }) {
  const router = useRouter();
  return <RouterProvider navigate={router.push}>{children}</RouterProvider>;
}
```

- `I18nProvider locale="…"` sets the locale for formatting and keyboard direction; set `lang` and `dir` on `<html>` yourself (`useLocale()` returns `{ locale, direction }`). Use logical utilities (`ms-*`, `pe-*`, `start-*`) so layouts mirror in RTL.
- Dates are `@internationalized/date` values (`parseDate`, `today(getLocalTimeZone())`), never `Date`. Numbers format through `NumberField`'s `formatOptions`, not by hand.

## Hooks

When no component fits, build with hooks from `react-aria` (`useButton`, `usePress`, `useFocusRing`, `useHover`, `mergeProps`) and state from `react-stately` (`useToggleState`, `useOverlayTriggerState`). Merge every hook's props with `mergeProps` onto one element.

## Do not

- Do not use `onClick`, `disabled`, `checked` or `onSelect`-style props from other libraries, or wrap Aria components in `<label>` or `<div role>` markup.
- Do not write new code with `Checkbox`, `Radio`, `Switch`, `Section`, `selectedKey` or `onSelectionChange` on Select and ComboBox.
- Do not mix in Radix `asChild` or Base UI `render={<Element />}`: React Aria's `render` is a function only.
- Do not import from internal `dist` paths, or reach for `forwardRef` in React 19 wrappers.

## Before you finish

- Run the type checker and the linter and fix what they report; heed deprecation warnings.
- Use every component you touched with the keyboard alone (Tab, arrows, Home, End, typeahead, Enter, Space, Escape) and check focus rings appear only for keyboard use.
- Check labels and errors are announced, overlays trap and return focus, and the layout mirrors with `dir="rtl"`.
