react-spectrum-s2

Builds user interfaces with Adobe's React Spectrum design system components.

Installation
Run `npx skills add "https://react-spectrum.adobe.com" --skill "react-spectrum-s2"` to install this skill, then follow its SKILL.md instructions for my next request.

Paste this into Claude Code, Cursor, or any agent that can run commands.

SKILL.mdShow the author's original SKILL.md
---
name: "react-spectrum-s2"
description: "Build UIs with React Spectrum S2 (Spectrum 2), Adobe's component library for React. Use when developers are using `@react-spectrum/s2` or need Adobe design system components. Includes React Aria Components docs as a reference for building custom components on top of unstyled primitives."
license: "Apache-2.0"
compatibility: "Requires a React project with @react-spectrum/s2 installed."
metadata:
  author: "Adobe"
  website: "https://react-spectrum.adobe.com/"
---

# React Spectrum S2 (Spectrum 2)

If the requirements do not clearly specify which React Spectrum component to use, consult the [Component Decision Tree](references/guides/component-decision-tree.md) before choosing a component.

If the request involves a Figma design, frame, or URL — or if the Figma MCP (`get_design_context`,`search_design_system`, etc.) is available — consult [Implementing Figma designs with React Spectrum S2](references/guides/figma-to-s2.md) before generating code.

When writing tests that exercise S2 components, consult [Testing with React Spectrum S2](references/guides/test-utils-guidance.md) and prefer the ARIA pattern testers from `@react-spectrum/test-utils` over hand-rolled role/selector queries.

## React Spectrum S2 vs React Aria Components

React Spectrum S2 is built on top of React Aria Components. The S2 components add Spectrum 2 styling, behavior, and slot structure on top of the unstyled React Aria primitives. Always prefer S2 components for React Spectrum work because they are pre-styled, design-system compliant, and cover most common UI patterns.

Only reach for React Aria Components directly when:
- Building a custom component because no S2 component matches the requirements. Follow [Creating Custom Components](references/guides/creating-custom-components.md) and pair the React Aria primitive with the S2 `style` macro for Spectrum styling.
- You need a utility such as `FocusScope`, `VisuallyHidden`, `useFocusRing`, `mergeProps`, etc.

The React Aria Components documentation is bundled under `references/react-aria/`. Many unstyled React Aria Components share the same name as S2 components, so ensure that you're searching and accessing the correct docs where needed.

## Imports

Use subpath imports matching the documentation — don't import from the package barrel `'@react-spectrum/s2'`:

```tsx
import {Button} from '@react-spectrum/s2/Button';
import {Card, CardPreview, Image, Content, Text, Footer} from '@react-spectrum/s2/Card';
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
import Folder from '@react-spectrum/s2/icons/Folder';
import CloudUpload from '@react-spectrum/s2/illustrations/gradient/generic2/CloudUpload';
```

Common types and list-data hooks are re-exported from `@react-spectrum/s2` — prefer importing from there instead of `react-aria-components`, `react-stately`, or `@react-types/*`:

```tsx
import type {Key, Selection, SortDescriptor, PressEvent, RangeValue, DateValue, DateRange, TimeValue, RouterConfig} from '@react-spectrum/s2';
import {useListData, useTreeData, useAsyncList} from '@react-spectrum/s2';
```

### Use the typed Item for each collection

Each S2 collection component has its own item export — there is no generic `Item` component. `Menu` → `MenuItem`/`MenuSection`; `Picker` → `PickerItem`/`PickerSection`; `ComboBox` → `ComboBoxItem`/`ComboBoxSection`; `ListView` → `ListViewItem`; `TreeView` → `TreeViewItem` (with `TreeViewItemContent`); `TableView` → `Row`/`Column`/`Cell`/`TableHeader`/`TableBody`; `SegmentedControl` → `SegmentedControlItem`; `TagGroup` → `Tag`; `Breadcrumbs` → `Breadcrumb`; `Accordion` → `AccordionItem` (with `AccordionItemHeader`/`AccordionItemTitle`/`AccordionItemPanel`).

## Styling

Use S2 components and the S2 `style` macro as the default styling approach.

- Prefer S2 components first; use their `styles` prop only for layout-style properties.
- For generic layouts (flex, grid, etc.), use native HTML elements with the `style` macro.
- Avoid using Tailwind, `radix-ui`, `shadcn/ui`, or any other third-party design system in S2 implementations.
- IMPORTANT: avoid using `UNSAFE_style` and `UNSAFE_className`.

S2 components take a `styles` prop (plural) restricted to layout properties:

- `margin`, `marginStart`, `marginEnd`, `marginTop`, `marginBottom`, `marginX`, `marginY`
- `width`, `minWidth`, `maxWidth`
- `flexGrow`, `flexShrink`, `flexBasis`
- `justifySelf`, `alignSelf`, `order`
- `gridArea`, `gridRow`, `gridRowStart`, `gridRowEnd`, `gridColumn`, `gridColumnStart`, `gridColumnEnd`
- `position`, `zIndex`, `top`, `bottom`, `inset`, `insetX`, `insetY`, `insetStart`, `insetEnd`
- `visibility`
- `height`, `minHeight`, `maxHeight` (only in specific components without an intrinsic height)

```tsx
<Button styles={style({marginStart: 8})}>Edit</Button>
```

Native HTML elements and React Aria Components take `className={style(...)}` — there the macro is unrestricted:

```tsx
import {style} from '@react-spectrum/s2/style' with {type: 'macro'};
import {Checkbox} from 'react-aria-components';

<div className={style({display: 'grid', gap: 12, padding: 16, backgroundColor: 'gray-75'})}>
  <h2 className={style({font: 'heading-sm'})}>Preferences</h2>
  <Checkbox
    className={style({
      display: 'flex',
      alignItems: 'center',
      gap: 8,
      color: {
        default: 'neutral',
        isSelected: 'blue-900'
      }
    })}
  />
</div>
```

Spacing values follow a 4px grid (`0`, `2`, `4`, `8`, `12`, `16`, …).

### One `style({...})` call per element

Express runtime variation **inside** a single `style({...})` call using its conditional system, not by producing multiple style strings and combining them on the element:

- Conditional keys can be nested objects (`variant: {primary: ..., secondary: ...}`) or boolean conditions starting with `is`/`allows` (`isSelected`, `allowsRemoving`, `isHovered`). The macro returns a function — call it with the runtime values: `styles({variant, isSelected})`.
- Don't concatenate macro results via template literals, `clsx`, `classnames`, or string spaces — the class names encode style precedence and concatenation breaks it. Use `mergeStyles` if you genuinely need to merge two separate style strings at runtime.
- Don't combine `className={style({...})}` (or `styles`) with an inline `style={{...}}` on the same element. The inline prop bypasses tokens and breaks layer ordering. Use the inline `style` prop only for values that genuinely can't be known at build time (e.g. a drag-handler position).

```tsx
// ❌ Two near-identical style calls combined at the call site.
const base = style({padding: 8, backgroundColor: 'gray-100'});
const active = style({backgroundColor: 'accent'});
<div className={`${base} ${isActive ? active : ''}`} />

// ✅ One call with a runtime condition.
const card = style({
  padding: 8,
  backgroundColor: {
    default: 'gray-100',
    isActive: 'accent'
  }
});
<div className={card({isActive})} />
```

If a value seems impossible to express in the macro, check the [Style Macro](references/guides/style-macro.md) reference before falling back to inline styles — most CSS properties (grid placement, overflow, position, sizing, display) are supported.

### Style macro values are tokens, not raw CSS

The macro is a typed token system. Raw CSS strings fail type-checking and push the agent toward inline-style workarounds. Common confusions:

- `width`/`height`/`maxWidth`/`maxHeight`/`minWidth`/`minHeight`: `'full'` (not `'100%'`), `'screen'`, or a number.
- `alignItems`/`justifyContent`/`alignSelf`/`justifySelf`: `'start'` / `'center'` / `'end'` / `'space-between'` (not `'flex-start'` / `'flex-end'`).
- Spacing (`margin`, `padding`, `gap`, `top`, etc.): numeric pixels on the 4px grid, not strings like `'1rem'`.
- `borderRadius`: `'none'`, `'sm'`, `'default'`, `'lg'`, `'xl'`, `'full'`, `'pill'`. There is no `'md'`.
- Colors: token names like `'gray-100'`, `'accent'`, `'neutral'`, `'neutral-subdued'` — not hex, `rgb()`, or `var(--…)`.
- Fonts: `font: 'heading-xl'`, `font: 'body-sm'`, etc. — not CSS font shorthand.
- Use **logical** sides — `paddingStart`/`paddingEnd`/`marginStart`/`marginEnd`/`insetStart`/`insetEnd`/`borderStartStartRadius` — not physical `paddingLeft`/`paddingRight`/`marginLeft`/`marginRight`/`borderTopLeftRadius`. The logical variants flip correctly under RTL.

Prefer **semantic** color tokens when the color carries meaning: `'accent'`, `'neutral'`, `'negative'`, `'positive'`, `'informative'`, `'notice'` for errors/success/info/warning/brand — not `'red-…'`/`'green-…'`/`'blue-…'`/`'orange-…'`. Reach for raw hue tokens only for decorative or chart colors.


### Don't restate default prop values

`variant="primary"` on `Button`, `size="M"` on most components, `density="regular"` on collections — setting a prop to its default is noise. Omit it.

## Responsive design

Layouts should adapt to viewport size. The `style` macro provides built-in breakpoint conditions on layout properties — use them rather than producing a fixed-width design.

Breakpoints (min-width, mobile-first): `xs` 480, `sm` 640, `md` 768, `lg` 1024, `xl` 1280, `2xl` 1536. The `default` value applies on the smallest viewport; each breakpoint takes over above its threshold.

```tsx
const grid = style({
  display: 'grid',
  gridTemplateColumns: {
    default: '1fr',
    sm: 'repeat(2, 1fr)',
    lg: 'repeat(3, 1fr)',
    xl: 'repeat(4, 1fr)'
  },
  gap: {default: 12, md: 16, lg: 24},
  padding: {default: 16, md: 24, lg: 32}
});
```

## Component composition

S2 components define their own internal DOM and slot structure. Don't inject wrapper elements where the component expects a specific child API.

- Don't wrap collection-item children in a `<div>`, `<span>`, `<section>`, etc. Use the slot components the component documents:
  - `TreeView` rows: `TreeViewItemContent`, not a `div`.
  - `ListView`, `Menu`, `Picker`, `ComboBox`, `Tabs`, etc.: `Text` (with `slot="label"`/`slot="description"` where relevant) plus documented icon/image/avatar slots.
  - `Card`, `AssetCard`, `UserCard`, `ProductCard`: prescribed slot components (`CardPreview`, `Image`, `Content`, `Text`, `Footer`, etc.).
- Extra layout inside a slot goes **inside** the slot component, not around it.
- Before composing a component, read the **API** section of its docs page — it lists expected child components, slots, and constraints. The docs examples are the source of truth.

### Buttons with text and icon

`Button`/`ActionButton`/`LinkButton` with **both** an icon and a text label require the label to be wrapped in `<Text>` — plain string children next to an icon render incorrectly. (Icon-only children render fine but require `aria-label`.) `Text` is re-exported from each button's own subpath:

```tsx
import {ActionButton, Text} from '@react-spectrum/s2/ActionButton';
import Download from '@react-spectrum/s2/icons/Download';

<ActionButton>
  <Download />
  <Text>Download</Text>
</ActionButton>
```

### Exceptions: components that accept arbitrary content

A few components are explicitly free-form: `TableView` `Cell`; Dialog/Popover bodies; `Disclosure`/`Accordion` panels. Compose these with native elements + the `style` macro like any page section. For any other component, defer to slot components.

### Only use `slot` values the component documents

Only pass `slot` when the parent's docs name that slot.

- Don't put `slot="close"` on a `Button` inside a `Dialog` `ButtonGroup` — the dialog handles its own close; `slot="close"` on something else causes the parent context to hide it.
- Don't sprinkle `slot="label"`/`slot="description"` on `Text` outside a component that exposes those slots.

### Don't reinvent `Card` / `CardView`

For grids of objects/files/products/people, use `CardView` plus a prescribed variant (`AssetCard`, `UserCard`, `ProductCard`) or `Card` composed with `CardPreview`/`Content`/`Text`/`Footer`. Don't emit hand-rolled card divs or `<article>` wrappers. Build a custom card with the documented slot components only when no variant fits.

### Collection components handle their own scrolling

`TableView`, `ListView`, `TreeView`, `CardView`, `Menu`, and `ListBox` virtualize and scroll internally. Don't wrap them in an `overflow`/`overflowY`/`overflowX` container — that produces a nested scroller and breaks keyboard navigation. Give the collection a bounded `height`/`maxHeight` via its `styles` prop instead.

```tsx
// ❌ Nested scroller around CardView.
<div className={style({overflowY: 'auto', height: 480})}><CardView>…</CardView></div>

// ✅ Let the collection size itself and scroll internally.
<CardView styles={style({height: 480})}>…</CardView>
```

## Collections

Collection components (`Menu`, `Picker`, `ComboBox`, `ListView`, `TreeView`, `TableView`, `CardView`, `SegmentedControl`, `TagGroup`, `Breadcrumbs`, etc.) share a small set of conventions. Getting them wrong causes runtime warnings, broken selection, and broken keyboard navigation.

### Every item needs an `id`

Items use `id` for selection, `onAction(key)`, sort, expansion, and React reconciliation. Static items get a literal `id`; dynamic items get `id={item.something}` inside the render function. When using `.map`, set **both** `id` and React's `key`:

```tsx
// ✅ With array.map — set BOTH `id` (for the collection) and `key` (for React).
<ListView aria-label="Files">
  {files.map(item => (
    <ListViewItem key={item.id} id={item.id}>{item.name}</ListViewItem>
  ))}
</ListView>
```

When passing data via the `items` prop and using a render function, only `id` is needed.

### `textValue` when item children aren't plain text

If an item's children include anything other than a plain string (icon, avatar, multi-slot layout, custom component), set `textValue="..."`. The collection uses it for screen-reader announcements, typeahead, and drag-and-drop labels. Omitting it produces a runtime warning.

```tsx
<ListViewItem id={item.id} textValue={item.name}>
  <FileIcon />
  <Text slot="label">{item.name}</Text>
  <Text slot="description">{item.size}</Text>
</ListViewItem>
```

### `aria-label` on the collection container

Every collection (`ListView`, `TableView`, `CardView`, `TreeView`, `Menu`, `ListBox`, `GridList`, `TagGroup`, `Breadcrumbs`) needs an accessible name — `aria-label="..."` or `aria-labelledby="..."`.

### Empty and loading states are built in

- Empty state: pass `renderEmptyState` returning an `IllustratedMessage`. Don't conditionally swap the whole collection for a custom empty `div`.
- Async data: use `useAsyncList` (or the user's preferred data fetching library) plus the collection's `loadingState`/`onLoadMore` props. Don't render a separate spinner.

### Bulk actions with ActionBar

Use the `renderActionBar` prop on a collection to show an `ActionBar` when items are selected. The collection passes the current `selectedKeys` to the callback and wires up count and clear-selection automatically — don't pass `selectedItemCount` or `onClearSelection` to `ActionBar` manually.

## Typography

`Text`, `Heading`, and `Content` are **slot components** — they only produce the right typography inside an S2 component that provides their slot context (cards, dialogs, lists, pickers, menus, tabs, etc.). Outside those contexts, use a native HTML element with the `style` macro.

```tsx
// ✅ Standalone heading.
<h1 className={style({font: 'heading-xl'})}>Project overview</h1>

// ❌ Standalone <Heading> with no slot context.
<Heading className={style({font: 'heading-xl'})}>Project overview</Heading>
```

The `font` shorthand sets `fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, **and** a default `color`. Don't add a redundant `color` next to a `font` value unless you actually want to override the default.

### Font scales — pick by role, not size

- `heading-*` — page/section headings (`h1`–`h6`); tracks the document outline.
- `title-*` — titles inside components (card titles, dialog titles, tile labels) that aren't part of the page outline.
- `body-*` — running prose and long-form copy.
- `detail-*` — supporting metadata, captions, timestamps, footnotes.
- `ui-*` — interactive UI text: buttons, menu items, form labels, table cells.
- `code-*` — inline or block code (monospace family).

Sizes are `-xs`, `-sm`, default (no suffix), `-lg`, `-xl`, `-2xl`, `-3xl` (not every family has every size — see the [Style Macro](references/guides/style-macro.md) reference).

## Provider scope

Mount a single `Provider` at the application root (inside `body`, around the top-level layout).

- Don't wrap `<html>`, `<head>`, or `<body>` with `Provider`.
- Don't add a `Provider` around a Dialog, Popover, Toast, or other portaled overlay — they inherit through context.
- Don't stack `Provider`s. Nesting is only correct when scoping a different `locale`/`router`/`colorScheme` to a subtree, which is rare.
- Let the `Provider` manage `colorScheme`. Don't hard-code `colorScheme="light"` or `colorScheme="dark"` to make a screenshot match — for one-off light/dark color differences, use `lightDark()`.

If your app uses `ToastQueue`, place a single `<ToastContainer />` as a sibling of the root layout, inside `Provider`.

```tsx
import {Provider} from '@react-spectrum/s2/Provider';
import {ToastContainer} from '@react-spectrum/s2/Toast';

function App() {
  return (
    <Provider>
      <ToastContainer />
      <YourApp />
    </Provider>
  );
}
```

## Form fields

S2 form fields render their own label, description, error message, and required indicator. Pass those as props on the field — don't wrap the field in a `<label>`/`<p>`/`<div>` to attach them.

- `label="..."` — visible label.
- `description="..."` — help text.
- `errorMessage="..."` (or a function) — validation error; pair with `isInvalid` / `validate` / `validationBehavior`.
- `isRequired` — marks required and shows the indicator.
- `necessityIndicator="icon" | "label"` — how required/optional is shown.
- `contextualHelp={<ContextualHelp>…</ContextualHelp>}` — help popover next to the label.
- `aria-label="..."` — when no visible label is needed.

```tsx
<TextField type="email" label="Email" description="We'll never share it." isRequired />
```

Group related fields with `Form`. It handles label alignment (`labelPosition`, `labelAlign`), default sizes, the required indicator, and submission/validation flow. `Form` is not limited to a single vertical column — wrap subsets of fields in a styled native element to build multi-column or grid layouts.

## Icons

Use S2's built-in icons and illustrations.

- Import icons from `@react-spectrum/s2/icons/...`, illustrations from `@react-spectrum/s2/illustrations/...`.
- Don't introduce third-party icon libraries (`lucide-react`, `phosphor-icons`, `heroicons`, etc.).
- Look up icons in the [Icons](references/components/icons.md) catalog (or the S2 MCP `search_s2_icons` tool if available). The catalog is the source of truth.
- Don't grep `node_modules` or the S2 source — slow, often misses the intended name, finds stale/internal matches.
- Search the **full** catalog; don't settle for a partial name match. `Heart` ≠ `HeartBroken`; `Edit` ≠ `EditIn`.

```tsx
import AlertTriangle from '@react-spectrum/s2/icons/AlertTriangle';
import DropToUpload from '@react-spectrum/s2/illustrations/gradient/generic1/DropToUpload';
import Warning from '@react-spectrum/s2/illustrations/linear/Warning';
```

Illustrations come in Gradient (Generic 1 / Generic 2 variants) and Linear styles.

Commonly used icons: `AlertTriangle`, `Close`, `ChevronDown`, `Checkmark`, `Preview`, `CheckmarkCircle`, `Add`, `ChevronUp`, `Data`, `FileText`, `InfoCircle`, `OpenIn`, `Chat`, `Code`.

See [Icons](references/components/icons.md) and [Illustrations](references/components/illustrations.md) for the full catalogs.

## Verify before declaring done

Before reporting the task as complete, exercise the project's own toolchain. The `style` macro performs build-time checks that the editor alone won't show.

- **Typecheck.** Run the project's typecheck (`tsc --noEmit`, `tsc -b`, etc.). Fix everything — wrong `size` values, missing required props, raw CSS in the macro all surface here.
- **Build or dev server.** Run at least once. The macro's "cannot statically evaluate" error means a value inside `style({...})` depends on something non-literal; refactor to use runtime conditions or the runtime style function.
- **Runtime warnings.** If you can render the page, check the console for missing `aria-label`/`textValue`, deprecated props, etc. Treat these as failures.

## Documentation Structure

The `references/` directory contains detailed documentation organized as follows:

### Guides
- [Component Decision Tree](references/guides/component-decision-tree.md): How to choose the right S2 component when requirements do not name one explicitly.
- [Implementing Figma designs with React Spectrum S2](references/guides/figma-to-s2.md): How to translate Figma designs (via the Figma MCP) into S2 components and the `style` macro.
- [Creating Custom Components](references/guides/creating-custom-components.md): How to build custom Spectrum 2 components using React Aria Components and the `style` macro.
- [Testing with React Spectrum S2](references/guides/test-utils-guidance.md): How to write tests for S2 components using ARIA pattern testers from `@react-spectrum/test-utils`.
- [AI Components](references/guides/ai-components.md)
- [Collections](references/guides/collections.md)
- [Drag and Drop](references/guides/dnd.md)
- [Forms](references/guides/forms.md)
- [Getting started](references/guides/getting-started.md)
- [Migrating to Spectrum 2](references/guides/migrating.md)
- [Selection](references/guides/selection.md)
- [Style Macro](references/guides/style-macro.md)
- [Styling](references/guides/styling.md)
- [Testing](references/guides/testing.md)
- [Working with AI](references/guides/ai.md)

### Components

Component documentation is in `references/components/` — one Markdown file per component (e.g. `references/components/Button.md`). Read the file for a component when you need its API, props, examples, or accessibility notes.

Available components: Accordion, ActionBar, ActionButton, ActionButtonGroup, ActionMenu, Avatar, AvatarGroup, Badge, Breadcrumbs, Button, ButtonGroup, Calendar, Card, CardView, Checkbox, CheckboxGroup, ColorArea, ColorField, ColorSlider, ColorSwatch, ColorSwatchPicker, ColorWheel, ComboBox, ContextualHelp, DateField, DatePicker, DateRangePicker, Dialog, Disclosure, Divider, DropZone, Form, Icons, IllustratedMessage, Illustrations, Image, InlineAlert, LabeledValue, Link, LinkButton, ListView, Menu, Meter, NumberField, Picker, Popover, ProgressBar, ProgressCircle, Provider, RadioGroup, RangeCalendar, RangeSlider, SearchField, SegmentedControl, SelectBoxGroup, SideNav, Skeleton, Slider, StatusLight, Switch, TableView, Tabs, TagGroup, TextArea, TextField, TimeField, Toast, ToggleButton, ToggleButtonGroup, Tooltip, TreeView.

### Additional References
- [React Aria Components](references/react-aria/llms.txt): Documentation for unstyled accessible primitives. Use only when no React Spectrum S2 component fits the requirements.

Mirrored from the author's public source. Install counts from the open skills registry.

The systems behind these skills get built for partners every week.

Partner with us