React Spectrum S2 (Spectrum 2)
If the requirements do not clearly specify which React Spectrum component to use, consult the Component Decision Tree before choosing a component.
If the request involves a Figma design, frame, or URL — or if the Figma MCP (
,
, etc.) is available — consult
Implementing Figma designs with React Spectrum S2 before generating code.
When writing tests that exercise S2 components, consult
Testing with React Spectrum S2 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 and pair the React Aria primitive with the S2 macro for Spectrum styling.
- You need a utility such as , , , , etc.
The React Aria Components documentation is bundled under
. 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
:
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
— prefer importing from there instead of
,
, or
:
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
component.
→
/
;
→
/
;
→
/
;
→
;
→
(with
);
→
/
/
/
/
;
→
;
→
;
→
;
→
(with
/
/
).
Styling
Use S2 components and the S2
macro as the default styling approach.
- Prefer S2 components first; use their prop only for layout-style properties.
- For generic layouts (flex, grid, etc.), use native HTML elements with the macro.
- Avoid using Tailwind, , , or any other third-party design system in S2 implementations.
- IMPORTANT: avoid using and .
S2 components take a
prop (plural) restricted to layout properties:
- , , , , , ,
- , ,
- , ,
- , ,
- , , , , , ,
- , , , , , , , ,
- , , (only in specific components without an intrinsic height)
tsx
<Button styles={style({marginStart: 8})}>Edit</Button>
Native HTML elements and React Aria Components take
— 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 (
,
,
,
,
,
, …).
One call per element
Express runtime variation
inside a single
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 / (, , ). The macro returns a function — call it with the runtime values: styles({variant, isSelected})
.
- Don't concatenate macro results via template literals, , , or string spaces — the class names encode style precedence and concatenation breaks it. Use if you genuinely need to merge two separate style strings at runtime.
- Don't combine (or ) with an inline on the same element. The inline prop bypasses tokens and breaks layer ordering. Use the inline 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 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:
- /////: (not ), , or a number.
- ///: / / / (not / ).
- Spacing (, , , , etc.): numeric pixels on the 4px grid, not strings like .
- : , , , , , , . There is no .
- Colors: token names like , , , — not hex, , or .
- Fonts: , , etc. — not CSS font shorthand.
- Use logical sides — ////// — not physical ////. The logical variants flip correctly under RTL.
Prefer
semantic color tokens when the color carries meaning:
,
,
,
,
,
for errors/success/info/warning/brand — not
/
/
/
. Reach for raw hue tokens only for decorative or chart colors.
Don't restate default prop values
on
,
on most components,
on collections — setting a prop to its default is noise. Omit it.
Responsive design
Layouts should adapt to viewport size. The
macro provides built-in breakpoint conditions on layout properties — use them rather than producing a fixed-width design.
Breakpoints (min-width, mobile-first):
480,
640,
768,
1024,
1280,
1536. The
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 , , , etc. Use the slot components the component documents:
- rows: , not a .
- , , , , , etc.: (with / where relevant) plus documented icon/image/avatar slots.
- , , , : prescribed slot components (, , , , , 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
/
/
with
both an icon and a text label require the label to be wrapped in
— plain string children next to an icon render incorrectly. (Icon-only children render fine but require
.)
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:
; Dialog/Popover bodies;
/
panels. Compose these with native elements + the
macro like any page section. For any other component, defer to slot components.
Only use values the component documents
Only pass
when the parent's docs name that slot.
- Don't put on a inside a — the dialog handles its own close; on something else causes the parent context to hide it.
- Don't sprinkle / on outside a component that exposes those slots.
Don't reinvent /
For grids of objects/files/products/people, use
plus a prescribed variant (
,
,
) or
composed with
/
/
/
. Don't emit hand-rolled card divs or
wrappers. Build a custom card with the documented slot components only when no variant fits.
Collection components handle their own scrolling
,
,
,
,
, and
virtualize and scroll internally. Don't wrap them in an
/
/
container — that produces a nested scroller and breaks keyboard navigation. Give the collection a bounded
/
via its
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 (
,
,
,
,
,
,
,
,
,
, etc.) share a small set of conventions. Getting them wrong causes runtime warnings, broken selection, and broken keyboard navigation.
Every item needs an
Items use
for selection,
, sort, expansion, and React reconciliation. Static items get a literal
; dynamic items get
inside the render function. When using
, set
both and React's
:
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
prop and using a render function, only
is needed.
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
. 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>
on the collection container
Every collection (
,
,
,
,
,
,
,
,
) needs an accessible name —
or
.
Empty and loading states are built in
- Empty state: pass returning an . Don't conditionally swap the whole collection for a custom empty .
- Async data: use (or the user's preferred data fetching library) plus the collection's / props. Don't render a separate spinner.
Bulk actions with ActionBar
Use the
prop on a collection to show an
when items are selected. The collection passes the current
to the callback and wires up count and clear-selection automatically — don't pass
or
to
manually.
Typography
,
, and
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
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
shorthand sets
,
,
,
,
and a default
. Don't add a redundant
next to a
value unless you actually want to override the default.
Font scales — pick by role, not size
- — page/section headings (–); tracks the document outline.
- — titles inside components (card titles, dialog titles, tile labels) that aren't part of the page outline.
- — running prose and long-form copy.
- — supporting metadata, captions, timestamps, footnotes.
- — interactive UI text: buttons, menu items, form labels, table cells.
- — inline or block code (monospace family).
Sizes are
,
, default (no suffix),
,
,
,
(not every family has every size — see the
Style Macro reference).
Provider scope
Mount a single
at the application root (inside
, around the top-level layout).
- Don't wrap , , or with .
- Don't add a around a Dialog, Popover, Toast, or other portaled overlay — they inherit through context.
- Don't stack s. Nesting is only correct when scoping a different // to a subtree, which is rare.
- Let the manage . Don't hard-code or to make a screenshot match — for one-off light/dark color differences, use .
If your app uses
, place a single
as a sibling of the root layout, inside
.
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
/
/
to attach them.
- — visible label.
- — help text.
- (or a function) — validation error; pair with / / .
- — marks required and shows the indicator.
necessityIndicator="icon" | "label"
— how required/optional is shown.
contextualHelp={<ContextualHelp>…</ContextualHelp>}
— help popover next to the label.
- — when no visible label is needed.
tsx
<TextField type="email" label="Email" description="We'll never share it." isRequired />
Group related fields with
. It handles label alignment (
,
), default sizes, the required indicator, and submission/validation flow.
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 (, , , etc.).
- Look up icons in the Icons catalog (or the S2 MCP tool if available). The catalog is the source of truth.
- Don't grep 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. ≠ ; ≠ .
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:
,
,
,
,
,
,
,
,
,
,
,
,
,
.
See Icons and Illustrations for the full catalogs.
Verify before declaring done
Before reporting the task as complete, exercise the project's own toolchain. The
macro performs build-time checks that the editor alone won't show.
- Typecheck. Run the project's typecheck (, , etc.). Fix everything — wrong 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 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 /, deprecated props, etc. Treat these as failures.
Documentation Structure
The
directory contains detailed documentation organized as follows:
Guides
- Component Decision Tree: How to choose the right S2 component when requirements do not name one explicitly.
- Implementing Figma designs with React Spectrum S2: How to translate Figma designs (via the Figma MCP) into S2 components and the macro.
- Creating Custom Components: How to build custom Spectrum 2 components using React Aria Components and the macro.
- Testing with React Spectrum S2: How to write tests for S2 components using ARIA pattern testers from
@react-spectrum/test-utils
.
- Collections
- Drag and Drop
- Forms
- Getting started
- Migrating to Spectrum 2
- Selection
- Style Macro
- Styling
- Testing
- Working with AI
Components
Component documentation is in
— 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: Documentation for unstyled accessible primitives. Use only when no React Spectrum S2 component fits the requirements.