home / skills / yonatangross / orchestkit / react-aria-patterns

react-aria-patterns skill

/plugins/ork/skills/react-aria-patterns

This skill helps you implement accessible React UI patterns using Adobe React Aria hooks for buttons, dialogs, comboboxes, and menus.

npx playbooks add skill yonatangross/orchestkit --skill react-aria-patterns

Review the files below or copy the command above to add this skill to your agents.

Files (5)
SKILL.md
6.7 KB
---
name: react-aria-patterns
description: React Aria (Adobe) accessible component patterns for building WCAG-compliant interactive UI with hooks. Use when implementing buttons, dialogs, comboboxes, menus, and other accessible components in React applications.
context: fork
agent: accessibility-specialist
version: 1.0.0
tags: [accessibility, react, aria, a11y, react-aria, wcag, hooks, adobe]
allowed-tools: [Read, Write, Grep, Glob, Bash]
author: OrchestKit
user-invocable: false
---

# React Aria Patterns

Build accessible UI components using Adobe's React Aria hooks library with React 19 patterns.

## Overview

- Building accessible buttons, links, and toggles with keyboard/screen reader support
- Implementing modal dialogs with proper focus management and trapping
- Creating autocomplete/combobox components with filtering and selection
- Building menu systems with roving tabindex and proper ARIA roles
- Implementing accessible tables, listboxes, and selection patterns

## Quick Reference

### useButton - Accessible Button Component

```tsx
import { useRef } from 'react';
import { useButton, useFocusRing, mergeProps } from 'react-aria';
import type { AriaButtonProps } from 'react-aria';

function Button(props: AriaButtonProps & { className?: string }) {
  const ref = useRef<HTMLButtonElement>(null);
  const { focusProps, isFocusVisible } = useFocusRing();
  const { buttonProps } = useButton(props, ref);

  return (
    <button
      {...mergeProps(buttonProps, focusProps)}
      ref={ref}
      className={`${props.className ?? ''} ${isFocusVisible ? 'ring-2 ring-blue-500' : ''}`}
    >
      {props.children}
    </button>
  );
}
```

### useDialog - Modal Dialog with Focus Management

```tsx
import { useRef } from 'react';
import { useDialog, useModalOverlay, FocusScope, mergeProps } from 'react-aria';
import { useOverlayTriggerState } from 'react-stately';

function Modal({ state, title, children }) {
  const ref = useRef<HTMLDivElement>(null);
  const { modalProps, underlayProps } = useModalOverlay({}, state, ref);
  const { dialogProps, titleProps } = useDialog({ 'aria-label': title }, ref);

  return (
    <div {...underlayProps} className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center">
      <FocusScope contain restoreFocus autoFocus>
        <div {...mergeProps(modalProps, dialogProps)} ref={ref} className="bg-white rounded-lg p-6">
          <h2 {...titleProps} className="text-xl font-semibold mb-4">{title}</h2>
          {children}
        </div>
      </FocusScope>
    </div>
  );
}
```

### useComboBox - Accessible Autocomplete

```tsx
import { useRef } from 'react';
import { useComboBox, useFilter } from 'react-aria';
import { useComboBoxState } from 'react-stately';

function ComboBox(props) {
  const { contains } = useFilter({ sensitivity: 'base' });
  const state = useComboBoxState({ ...props, defaultFilter: contains });
  const inputRef = useRef(null), buttonRef = useRef(null), listBoxRef = useRef(null);

  const { buttonProps, inputProps, listBoxProps, labelProps } = useComboBox(
    { ...props, inputRef, buttonRef, listBoxRef }, state
  );

  return (
    <div className="relative inline-flex flex-col">
      <label {...labelProps}>{props.label}</label>
      <div className="flex">
        <input {...inputProps} ref={inputRef} className="border rounded-l px-3 py-2" />
        <button {...buttonProps} ref={buttonRef} className="border rounded-r px-2">&#9660;</button>
      </div>
      {state.isOpen && (
        <ul {...listBoxProps} ref={listBoxRef} className="absolute top-full w-full border bg-white">
          {[...state.collection].map((item) => (
            <li key={item.key} className="px-3 py-2 hover:bg-gray-100">{item.rendered}</li>
          ))}
        </ul>
      )}
    </div>
  );
}
```

## Key Decisions

| Decision | Option A | Option B | Recommendation |
|----------|----------|----------|----------------|
| Hook vs Component | `useButton` hooks | `Button` from react-aria-components | **Hooks** for control, Components for speed |
| Focus Management | Manual `tabIndex` | `FocusScope` component | **FocusScope** - trapping, restore, auto-focus |
| Virtual Lists | Native scroll | `useVirtualizer` + `useListBox` | **Virtualizer** for lists > 100 items |
| State Management | Local useState | react-stately hooks | **react-stately** - designed for a11y |

## Anti-Patterns (FORBIDDEN)

```tsx
// NEVER use div with onClick for interactive elements
<div onClick={handleClick}>Click me</div>  // Missing keyboard support!

// ALWAYS use useButton or native button
const { buttonProps } = useButton({ onPress: handleClick }, ref);
<div {...buttonProps} ref={ref}>Click me</div>

// NEVER handle focus manually for modals
useEffect(() => { modalRef.current?.focus(); }, []);  // Incomplete!

// ALWAYS use FocusScope for modals/overlays
<FocusScope contain restoreFocus autoFocus>
  <div role="dialog">...</div>
</FocusScope>

// NEVER forget aria-live for dynamic announcements
<div>{errorMessage}</div>  // Screen readers won't announce!

// ALWAYS use aria-live for status updates
<div aria-live="polite" className="sr-only">{errorMessage}</div>

// NEVER omit label associations
<input type="text" placeholder="Email" />  // No accessible name!

// ALWAYS associate labels properly
<label {...labelProps}>Email</label>
<input {...inputProps} />
```

## Related Skills

- `a11y-testing` - Automated accessibility testing with jest-axe and Playwright
- `focus-management` - Advanced focus patterns and keyboard navigation
- `design-system-starter` - Building accessible component libraries
- `i18n-date-patterns` - Internationalization for accessible content

## Capability Details

### useButton-hook
**Keywords:** button, useButton, press, tap, keyboard, click, onPress, focus ring
**Solves:**
- How to create accessible custom buttons
- Handling keyboard and pointer interactions consistently
- Focus ring visibility management with useFocusRing

### useDialog-modal
**Keywords:** dialog, modal, useDialog, useModalOverlay, FocusScope, overlay, trap
**Solves:**
- Building accessible modal dialogs with proper ARIA roles
- Focus trapping within overlays using FocusScope
- Restoring focus to trigger element on close

### useComboBox-autocomplete
**Keywords:** combobox, autocomplete, useComboBox, typeahead, filter, select, dropdown
**Solves:**
- Accessible autocomplete/typeahead inputs with filtering
- Keyboard navigation through options (arrow keys, enter, escape)
- Screen reader announcements for selection changes

### focus-scope-management
**Keywords:** focus, FocusScope, contain, restore, autoFocus, trap, keyboard navigation
**Solves:**
- Trapping focus within modals and popovers (contain prop)
- Restoring focus to trigger elements on unmount (restoreFocus prop)
- Auto-focusing first focusable element (autoFocus prop)

Overview

This skill provides production-ready React Aria patterns and TypeScript hooks for building WCAG-compliant interactive UI in React 19. It packages accessible implementations for buttons, dialogs, comboboxes, menus, lists, and other common components. Use these patterns to enforce keyboard support, proper ARIA roles, and reliable focus management across your app.

How this skill works

Patterns wrap Adobe React Aria and react-stately hooks into concise component and hook examples that handle keyboard, pointer, and screen reader behavior. Each pattern wires up useButton, useDialog, useComboBox, FocusScope, and related hooks to manage focus trapping, aria attributes, roving tabindex, and filtering. Code examples show refs, mergeProps, and state integration so you can drop them into TypeScript projects with minimal adaptation.

When to use it

  • When building custom interactive controls that must support keyboard and screen reader users.
  • When implementing modal dialogs, popovers, or overlays that must trap and restore focus.
  • When adding autocomplete/combobox inputs with accessible filtering and selection.
  • When constructing menus, listboxes, or virtualized lists with proper ARIA roles.
  • When creating a design system or component library requiring consistent a11y patterns.

Best practices

  • Prefer hooks (useButton, useDialog, useComboBox) for full control; use components for rapid delivery.
  • Always use FocusScope for overlays: contain, restoreFocus, and autoFocus for correct focus behavior.
  • Avoid using non-semantic elements (div/span) for interactive controls; use native semantics or useButton.
  • Add aria-live regions for dynamic announcements like validation or selection changes.
  • Use a virtualizer for lists larger than ~100 items to keep keyboard navigation performant.

Example use cases

  • Custom-styled button component that preserves keyboard press, focus ring, and screen reader name.
  • Modal confirmation dialog that traps focus, restores focus on close, and exposes accessible title.
  • Autocomplete search box with typeahead filtering, arrow-key navigation, and screen reader hints.
  • Accessible menu bar or context menu with roving tabindex and correct ARIA roles.
  • Large selectable table or list using virtual scrolling combined with useListBox for a11y.

FAQ

Do I need react-stately too?

Yes. react-stately provides the state primitives the patterns rely on for selection and open/close behavior; use both for the recommended approach.

Can I use these patterns with Tailwind or other CSS frameworks?

Yes. Patterns focus on behavior and ARIA wiring; styling is independent so you can apply Tailwind, CSS modules, or utility classes.