关于 react-spectrum-s2
为使用 React Spectrum S2 构建界面的编码 Agent 提供指导,涵盖组件选择、导入、样式、组合、集合、无障碍设计和响应式布局。
主要功能
- 01S2 组件选择指南
- 02子路径导入约定
- 03样式宏与令牌规则
- 04组件组合和集合模式
- 05无障碍与响应式布局指南
适用场景
- 01按界面需求选择 S2 组件
- 02使用 S2 实现 Figma 设计
- 03构建无障碍的 S2 集合
- 04为 S2 组件编写测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ zzci/ skills --skill react-spectrum-s2安装后在 Claude Code 中输入/react-spectrum-s2确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/components/Accordion.mdActionBar.mdActionButton.mdActionButtonGroup.mdActionMenu.mdAvatar.mdAvatarGroup.mdBadge.mdBreadcrumbs.mdButton.mdButtonGroup.mdCalendar.mdCard.mdCardView.mdCheckbox.mdCheckboxGroup.mdColorArea.mdColorField.mdColorSlider.mdColorSwatch.mdColorSwatchPicker.mdColorWheel.mdComboBox.mdContextualHelp.mdDateField.mdDatePicker.mdDateRangePicker.mdDialog.mdDisclosure.mdDivider.md详细介绍
React Spectrum S2(Spectrum 2)
如果需求没有明确指定要使用哪个 React Spectrum 组件,请先查阅 组件决策树,再选择组件。
如果请求涉及 Figma 设计、画板或 URL,或者可使用 Figma MCP(get_design_context,search_design_system 等),请先查阅使用 React Spectrum S2 实现 Figma 设计,再生成代码。
编写测试以测试 S2 组件时,请查阅使用 React Spectrum S2 进行测试,并优先使用 @react-spectrum/test-utils 中的 ARIA 模式测试器,而不要手动编写 role/selector 查询。
React Spectrum S2 与 React Aria Components
React Spectrum S2 基于 React Aria Components 构建。S2 组件在无样式的 React Aria 基元之上添加了 Spectrum 2 样式、行为和插槽结构。React Spectrum 项目应始终优先使用 S2 组件,因为它们已预设样式、符合设计系统规范,并涵盖大多数常见 UI 模式。
仅在以下情况直接使用 React Aria Components:
- 构建没有匹配需求的 S2 组件的自定义组件。请遵循创建自定义组件,并将 React Aria 基元与 S2
style宏配合使用,以应用 Spectrum 样式。 - 需要使用
FocusScope、VisuallyHidden、useFocusRing、mergeProps等工具。
React Aria Components 文档位于 references/react-aria/ 下。许多无样式的 React Aria Components 与 S2 组件同名,因此请确保在需要时搜索并访问正确的文档。
导入
请使用与文档匹配的子路径导入,不要从包入口 '@react-spectrum/s2' 导入:
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';
常用类型和列表数据钩子会从 @react-spectrum/s2 重新导出;请优先从此处导入,而不是从 react-aria-components、react-stately 或 @react-types/* 导入:
import type {Key, Selection, SortDescriptor, PressEvent, RangeValue, DateValue, DateRange, TimeValue, RouterConfig} from '@react-spectrum/s2';
import {useListData, useTreeData, useAsyncList} from '@react-spectrum/s2';
为每个集合使用对应的类型化 Item
每个 S2 集合组件都有自己的 item 导出,不存在通用的 Item 组件。Menu → MenuItem/MenuSection;Picker → PickerItem/PickerSection;ComboBox → ComboBoxItem/ComboBoxSection;ListView → ListViewItem;TreeView → TreeViewItem(配合 TreeViewItemContent);TableView → Row/Column/Cell/TableHeader/TableBody;SegmentedControl → SegmentedControlItem;TagGroup → Tag;Breadcrumbs → Breadcrumb;Accordion → AccordionItem(配合 AccordionItemHeader/AccordionItemTitle/AccordionItemPanel)。
样式
默认使用 S2 组件和 S2 style 宏来设置样式。
- 优先使用 S2 组件;其
styles属性仅用于布局样式属性。 - 对于通用布局(flex、grid 等),使用带有
style宏的原生 HTML 元素。 - 在 S2 实现中,避免使用 Tailwind、
radix-ui、shadcn/ui或任何其他第三方设计系统。 - 重要:避免使用
UNSAFE_style和UNSAFE_className。
S2 组件接受 styles 属性(复数形式),且仅限布局属性:
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(仅适用于没有固有高度的特定组件)
<Button styles={style({marginStart: 8})}>Edit</Button>
原生 HTML 元素和 React Aria Components 接受 className={style(...)},此处的宏不受限制:
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>
间距值遵循 4px 网格(0、2、4、8、12、16,……)。
每个元素只调用一次 style({...})
使用 style({...}) 单次调用中的条件系统表达内部的运行时变化,不要生成多个样式字符串再在元素上组合:
- 条件键可以是嵌套对象(
variant: {primary: ..., secondary: ...}),也可以是以is/allows开头的布尔条件(isSelected、allowsRemoving、isHovered)。宏会返回一个函数——用运行时值调用它:styles({variant, isSelected})。 - 不要通过模板字面量、
clsx、classnames或字符串空格拼接宏的结果——类名编码了样式优先级,拼接会破坏这一点。如果确实需要在运行时合并两个独立的样式字符串,请使用mergeStyles。 - 不要在同一个元素上同时使用
className={style({...})}(或styles)和内联的style={{...}}。内联属性会绕过令牌并破坏层级顺序。只有对于确实无法在构建时确定的值(例如拖动处理程序的位置),才使用内联style属性。
// ❌ 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})} />
如果某个值似乎无法通过宏表达,请先查阅 Style Macro 参考文档,再考虑使用内联样式——宏支持大多数 CSS 属性(网格布局、溢出、定位、尺寸、显示)。
样式宏的值是令牌,不是原始 CSS
宏采用类型化令牌系统。原始 CSS 字符串无法通过类型检查,会让 Agent 转而使用内联样式作为变通方案。常见易混淆之处:
width/height/maxWidth/maxHeight/minWidth/minHeight:使用'full'(而不是'100%')、'screen'或数字。alignItems/justifyContent/alignSelf/justifySelf:使用'start'/'center'/'end'/'space-between'(而不是'flex-start'/'flex-end')。- 间距(
margin、padding、gap、top等):使用 4px 网格上的像素数值,而不是'1rem'这样的字符串。 borderRadius:使用'none'、'sm'、'default'、'lg'、'xl'、'full'、'pill'。没有'md'。- 颜色:使用
'gray-100'、'accent'、'neutral'、'neutral-subdued'等令牌名称,而不是十六进制值、rgb()或var(--…)。 - 字体:使用
font: 'heading-xl'、font: 'body-sm'等,而不是 CSS 字体简写。 - 使用逻辑边(
paddingStart/paddingEnd/marginStart/marginEnd/insetStart/insetEnd/borderStartStartRadius),而不是物理边(paddingLeft/paddingRight/marginLeft/marginRight/borderTopLeftRadius)。逻辑边变体在 RTL 下会正确翻转。
当颜色具有语义时,优先使用语义颜色令牌:对于错误/成功/信息/警告/品牌,使用 'accent'、'neutral'、'negative'、'positive'、'informative'、'notice',而不是 'red-…'/'green-…'/'blue-…'/'orange-…'。仅在装饰或图表颜色场景下使用原始色相令牌。
不要重复声明默认属性值
Button 上的 variant="primary"、大多数组件上的 size="M"、集合上的 density="regular"——将属性设为默认值没有意义。省略即可。
响应式设计
布局应适应视口大小。style 宏在布局属性上内置了断点条件——应使用这些条件,而不是设计固定宽度布局。
断点(最小宽度,移动优先):xs 480、sm 640、md 768、lg 1024、xl 1280、2xl 1536。最小视口使用 default 值;超过各断点阈值后,则由对应断点接管。
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}
});
组件组合
S2 组件会定义自己的内部 DOM 和插槽结构。当组件要求特定的子组件 API 时,不要注入包装元素。
- 不要用
<div>、<span>、<section>等元素包装集合项的子元素。请使用组件文档中说明的插槽组件:TreeView行:使用TreeViewItemContent,而不是div。ListView、Menu、Picker、ComboBox、Tabs等:使用Text(在相关情况下添加slot="label"/slot="description"),以及文档中说明的图标/图片/头像插槽。Card、AssetCard、UserCard、ProductCard:使用指定的插槽组件(CardPreview、Image、Content、Text、Footer等)。
- 插槽内的额外布局应放在插槽组件内部,而不是包在插槽组件外部。
- 组合组件前,请阅读其文档页面的 API 部分——其中列出了预期的子组件、插槽和限制。文档示例是权威依据。
同时带文本和图标的按钮
同时带有图标和文本标签的 Button/ActionButton/LinkButton,要求将标签包在 <Text> 中——图标旁的纯字符串子元素无法正确渲染。(仅带图标的子元素可以正常渲染,但需要 aria-label。)Text 会从每个按钮自己的子路径重新导出:
import {ActionButton, Text} from '@react-spectrum/s2/ActionButton';
import Download from '@react-spectrum/s2/icons/Download';
<ActionButton>
<Download />
<Text>Download</Text>
</ActionButton>
例外:接受任意内容的组件
有少数组件明确支持自由形式的内容:TableView 的 Cell;Dialog/Popover 的主体;Disclosure/Accordion 面板。可以像页面的其他部分一样,使用原生元素和 style 宏组合这些内容。对于其他组件,请使用插槽组件。
仅使用组件文档中说明的 slot 值
只有当父组件的文档中列出该插槽时,才传递 slot。
- 不要在 Dialog
ButtonGroup内的Button上设置slot="close"——对话框会自行处理关闭;在其他元素上设置slot="close"会导致父级上下文将其隐藏。 - 不要在未提供这些插槽的组件之外,随意给
Text添加slot="label"/slot="description"。
不要重新实现 Card/CardView
对于对象/文件/产品/人员的网格,请使用 CardView 和指定的变体(AssetCard、UserCard、ProductCard),或使用 Card 搭配 CardPreview/Content/Text/Footer。不要手动编写卡片 div 或 <article> 包装元素。只有在没有合适变体时,才使用文档中说明的插槽组件构建自定义卡片。
集合组件会自行处理滚动
TableView、ListView、TreeView、CardView、Menu 和 ListBox 会自行虚拟化并处理滚动。不要将它们包在 overflow/overflowY/overflowX 容器中——这会产生嵌套滚动区域,并破坏键盘导航。请通过组件的 styles 属性为集合设置有界的 height/maxHeight。
// ❌ 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>
集合
集合组件(Menu、Picker、ComboBox、ListView、TreeView、TableView、CardView、SegmentedControl、TagGroup、Breadcrumbs 等)遵循一小组通用约定。违反这些约定会导致运行时警告、选择功能异常和键盘导航失效。
每个项都需要 id
项使用 id 来处理选择、onAction(key)、排序、展开和 React 协调。静态项使用字面量 id;动态项在渲染函数中使用 id={item.something}。使用 .map 时,请同时设置 id 和 React 的 key:
// ✅ 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>
通过 items 属性传递数据并使用渲染函数时,只需要 id。
当项的子元素不是纯文本时,设置 textValue
如果项的子元素包含纯字符串以外的内容(图标、头像、多插槽布局、自定义组件),请设置 textValue="..."。集合会使用它生成屏幕阅读器播报、类型前缀搜索和拖放标签。省略该属性会产生运行时警告。
<ListViewItem id={item.id} textValue={item.name}>
<FileIcon />
<Text slot="label">{item.name}</Text>
<Text slot="description">{item.size}</Text>
</ListViewItem>
在集合容器上设置 aria-label
每个集合(ListView、TableView、CardView、TreeView、Menu、ListBox、GridList、TagGroup、Breadcrumbs)都需要一个可访问名称——aria-label="..." 或 aria-labelledby="..."。
空状态和加载状态已内置
- 空状态:传入返回
IllustratedMessage的renderEmptyState。不要有条件地将整个集合替换为自定义空状态div。 - 异步数据:使用
useAsyncList(或用户偏好的数据获取库),并配合集合的loadingState/onLoadMore属性。不要单独渲染加载指示器。
使用 ActionBar 执行批量操作
在集合上使用 renderActionBar 属性,在选中项目时显示 ActionBar。集合会将当前的 selectedKeys 传递给回调,并自动处理计数和清除选择;不要手动向 ActionBar 传入 selectedItemCount 或 onClearSelection。
字体排版
Text、Heading 和 Content 是插槽组件——只有在提供插槽上下文的 S2 组件(卡片、对话框、列表、选择器、菜单、标签页等)内,它们才会呈现正确的字体排版。在这些上下文之外,请使用带有 style 宏的原生 HTML 元素。
// ✅ 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>
font 简写会设置 fontFamily、fontSize、fontWeight、lineHeight 以及默认 color。除非确实需要覆盖默认值,否则不要在 font 值旁重复添加 color。
字体比例——按用途选择,而非按字号
heading-*——页面/区块标题(h1–h6);与文档大纲保持一致。title-*——组件内部的标题(卡片标题、对话框标题、磁贴标签),不属于页面大纲。body-*——正文和长篇文案。detail-*——辅助元数据、说明文字、时间戳、脚注。ui-*——交互式 UI 文本:按钮、菜单项、表单标签、表格单元格。code-*——行内或代码块(等宽字体)。
字号为 -xs、-sm、默认值(无后缀)、-lg、-xl、-2xl、-3xl(并非每种字体系列都包含所有字号——请参阅 Style Macro 参考)。
Provider 作用范围
在应用根部(body 内、顶层布局周围)挂载一个 Provider。
- 不要用
Provider包裹<html>、<head>或<body>。 - 不要在 Dialog、Popover、Toast 或其他通过 Portal 渲染的叠加层外添加
Provider——它们会通过上下文继承。 - 不要堆叠多个
Provider。只有在子树范围内设置不同的locale/router/colorScheme时才应嵌套,这种情况很少见。 - 让
Provider管理colorScheme。不要为了匹配截图而硬编码colorScheme="light"或colorScheme="dark"——对于个别浅色/深色差异,请使用lightDark()。
如果应用使用 ToastQueue,请在根布局旁、Provider 内放置一个 <ToastContainer />。
import {Provider} from '@react-spectrum/s2/Provider';
import {ToastContainer} from '@react-spectrum/s2/Toast';
function App() {
return (
<Provider>
<ToastContainer />
<YourApp />
</Provider>
);
}
表单字段
S2 表单字段会自行渲染标签、说明、错误消息和必填指示器。请将这些内容作为属性传给字段——不要用 <label>/<p>/<div> 包裹字段来附加它们。
label="..."——可见标签。description="..."——帮助文本。errorMessage="..."(或函数)——验证错误;与isInvalid/validate/validationBehavior配合使用。isRequired——标记为必填并显示指示器。necessityIndicator="icon" | "label"——指定如何显示必填/选填状态。contextualHelp={<ContextualHelp>…</ContextualHelp>}——标签旁的帮助弹出框。aria-label="..."——无需可见标签时使用。
<TextField type="email" label="Email" description="We'll never share it." isRequired />
使用 Form 将相关字段分组。它负责处理标签对齐(labelPosition、labelAlign)、默认尺寸、必填指示器以及提交/验证流程。Form 不限于单列纵向布局——可将部分字段包裹在经过样式设置的原生元素中,以构建多列或网格布局。
图标
使用 S2 内置的图标和插图。
- 从
@react-spectrum/s2/icons/...导入图标,从@react-spectrum/s2/illustrations/...导入插图。 - 不要引入第三方图标库(
lucide-react、phosphor-icons、heroicons等)。 - 在 图标 目录中查找图标(如果可用,也可以使用 S2 MCP
search_s2_icons工具)。该目录是唯一可信来源。 - 不要在
node_modules或 S2 源码中使用 grep——速度慢,而且经常找不到预期名称,反而会搜出过时或内部匹配项。 - 搜索完整目录;不要满足于部分名称匹配。
Heart≠HeartBroken;Edit≠EditIn。
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';
插图有渐变(Generic 1 / Generic 2 变体)和线性两种样式。
常用图标:AlertTriangle、Close、ChevronDown、Checkmark、Preview、CheckmarkCircle、Add、ChevronUp、Data、FileText、InfoCircle、OpenIn、Chat、Code。
完成前验证
在报告任务完成之前,请运行项目自身的工具链。style 宏会执行仅靠编辑器无法发现的构建时检查。
- 类型检查。 运行项目的类型检查(
tsc --noEmit、tsc -b等)。修复所有问题——错误的size值、缺少必需的属性、宏中的原始 CSS 等都会在这里暴露出来。 - 构建或开发服务器。 至少运行一次。宏报错“cannot statically evaluate”表示
style({...})中的某个值依赖于非字面量;请重构为使用运行时条件或运行时样式函数。 - 运行时警告。 如果可以渲染页面,请检查控制台中是否存在缺少
aria-label/textValue、属性已弃用等问题。请将这些问题视为失败。
文档结构
references/ 目录包含详细文档,组织如下:
指南
- 组件决策树:当需求未明确指定组件时,如何选择合适的 S2 组件。
- 使用 React Spectrum S2 实现 Figma 设计:如何通过 Figma MCP 将 Figma 设计转换为 S2 组件和
style宏。 - 创建自定义组件:如何使用 React Aria Components 和
style宏构建自定义 Spectrum 2 组件。 - 使用 React Spectrum S2 进行测试:如何使用
@react-spectrum/test-utils中的 ARIA 模式测试器,为 S2 组件编写测试。 - 集合
- 拖放
- 表单
- 快速入门
- 迁移到 Spectrum 2
- 选择
- 样式宏
- 样式设置
- 测试
- 使用 AI
组件
组件文档位于 references/components/ 中——每个组件对应一个 Markdown 文件(例如 references/components/Button.md)。需要了解组件的 API、属性、示例或无障碍说明时,请阅读相应文件。
可用组件: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。
补充参考
- React Aria Components:无样式、支持无障碍访问的基础组件文档。仅当 React Spectrum S2 中没有符合要求的组件时使用。