npx skills add ...
npx skills add https://react-spectrum.adobe.com --skill react-spectrum-s2
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.
npx skills add https://react-spectrum.adobe.com --skill react-spectrum-s2
If the requirements do not clearly specify which React Spectrum component to use, consult the Component Decision Tree [blocked] 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 [blocked] before generating code.
When writing tests that exercise S2 components, consult Testing with React Spectrum S2 [blocked] and prefer the ARIA pattern testers from @react-spectrum/test-utils over hand-rolled role/selector queries.
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:
style macro for Spectrum styling.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.
Use subpath imports matching the documentation — don't import from the package barrel '@react-spectrum/s2':
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/*:
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).
Use S2 components and the S2 style macro as the default styling approach.
styles prop only for layout-style properties.style macro.radix-ui, shadcn/ui, or any other third-party design system in S2 implementations.UNSAFE_style and UNSAFE_className.S2 components take a styles prop (plural) restricted to layout properties:
margin, marginStart, marginEnd, marginTop, marginBottom, marginX, marginYwidth, minWidth, maxWidthflexGrow, flexShrink, flexBasisjustifySelf, alignSelf, ordergridArea, gridRow, gridRowStart, gridRowEnd, gridColumn, gridColumnStart, gridColumnEndposition, zIndex, top, bottom, inset, insetX, insetY, insetStart, insetEndvisibilityheight, minHeight, maxHeight (only in specific components without an intrinsic height)Native HTML elements and React Aria Components take className={style(...)} — there the macro is unrestricted:
Spacing values follow a 4px grid (0, 2, 4, 8, 12, 16, …).
style({...}) call per elementExpress runtime variation inside a single style({...}) call using its conditional system, not by producing multiple style strings and combining them on the element:
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}).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.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).If a value seems impossible to express in the macro, check the Style Macro [blocked] reference before falling back to inline styles — most CSS properties (grid placement, overflow, position, sizing, display) are supported.
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').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'.'gray-100', 'accent', 'neutral', 'neutral-subdued' — not hex, rgb(), or var(--…).font: 'heading-xl', font: 'body-sm', etc. — not CSS font shorthand.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.
variant="primary" on Button, size="M" on most components, density="regular" on collections — setting a prop to its default is noise. Omit it.
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.
S2 components define their own internal DOM and slot structure. Don't inject wrapper elements where the component expects a specific child API.
<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.).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:
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.
slot values the component documentsOnly pass slot when the parent's docs name that slot.
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.slot="label"/slot="description" on Text outside a component that exposes those slots.Card / CardViewFor 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.
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.
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.
idItems 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:
When passing data via the items prop and using a render function, only id is needed.
textValue when item children aren't plain textIf 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.
aria-label on the collection containerEvery collection (ListView, TableView, CardView, TreeView, Menu, ListBox, GridList, TagGroup, Breadcrumbs) needs an accessible name — aria-label="..." or aria-labelledby="...".
renderEmptyState returning an IllustratedMessage. Don't conditionally swap the whole collection for a custom empty div.useAsyncList (or the user's preferred data fetching library) plus the collection's loadingState/onLoadMore props. Don't render a separate spinner.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.
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.
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.
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 [blocked] reference).
Mount a single Provider at the application root (inside body, around the top-level layout).
<html>, <head>, or <body> with Provider.Provider around a Dialog, Popover, Toast, or other portaled overlay — they inherit through context.Providers. Nesting is only correct when scoping a different locale/router/colorScheme to a subtree, which is rare.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.
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.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.
Use S2's built-in icons and illustrations.
@react-spectrum/s2/icons/..., illustrations from @react-spectrum/s2/illustrations/....lucide-react, phosphor-icons, heroicons, etc.).search_s2_icons tool if available). The catalog is the source of truth.node_modules or the S2 source — slow, often misses the intended name, finds stale/internal matches.Heart ≠ HeartBroken; Edit ≠ EditIn.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 [blocked] and Illustrations [blocked] for the full catalogs.
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.
tsc --noEmit, tsc -b, etc.). Fix everything — wrong size values, missing required props, raw CSS in the macro all surface here.style({...}) depends on something non-literal; refactor to use runtime conditions or the runtime style function.aria-label/textValue, deprecated props, etc. Treat these as failures.The references/ directory contains detailed documentation organized as follows:
style macro.style macro.@react-spectrum/test-utils.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.
import type {Key, Selection, SortDescriptor, PressEvent, RangeValue, DateValue, DateRange, TimeValue, RouterConfig} from '@react-spectrum/s2';
import {useListData, useTreeData, useAsyncList} from '@react-spectrum/s2';<Button styles={style({marginStart: 8})}>Edit</Button>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>// ❌ 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})} />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}
});import {ActionButton, Text} from '@react-spectrum/s2/ActionButton';
import Download from '@react-spectrum/s2/icons/Download';
<ActionButton>
<Download />
<Text>Download</Text>
</ActionButton>// ❌ 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>// ✅ 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><ListViewItem id={item.id} textValue={item.name}>
<FileIcon />
<Text slot="label">{item.name}</Text>
<Text slot="description">{item.size}</Text>
</ListViewItem>// ✅ 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>import {Provider} from '@react-spectrum/s2/Provider';
import {ToastContainer} from '@react-spectrum/s2/Toast';
function App() {
return (
<Provider>
<ToastContainer />
<YourApp />
</Provider>
);
}<TextField type="email" label="Email" description="We'll never share it." isRequired />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';