mcpField
C
components

components

github.com/paragonhq设计与 UI044 次安装5 周前更新
代码仓库

关于 components

components 是用于构建一致且易访问的界面组件的设计技能。它涵盖组件 API、交互状态、样式和测试,并建议复杂行为采用无头原语实现。

主要功能

  1. 01为复杂交互选择原语
  2. 02定义完整的交互状态
  3. 03组合受控与非受控 API
  4. 04使用设计令牌设置样式
  5. 05测试键盘、响应式和辅助技术使用

适用场景

  1. 01规划易访问的对话框或菜单
  2. 02检查组件状态覆盖情况
  3. 03统一组件 API 与样式

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/paragonhq/stealth-skills --skill components

安装后在 Claude Code 中输入/components确认已加载。

详细介绍

组件

组件是一种契约:无论在哪里看起来都一样、表现都一样,而且无需检查就值得信任。大多数生成的组件都无法做到第三点。

使用基础组件还是手工实现?

只有没有行为的组件才手工实现:卡片、徽章、分隔线、统计数据。任何涉及焦点管理、键盘交互、定位或 ARIA 的组件都使用无头基础组件。

需求基础组件不手工实现的原因
对话框、侧边面板、弹出框、菜单、选择器、工具提示、标签页、切换开关、滑块、组合框Base UI、Radix、React Aria、Ark焦点陷阱、Escape、外部点击、按键搜索、ARIA、portal、碰撞检测
可拖动抽屉Vaul手势物理效果、吸附点、滚动锁定
ToastSonner堆叠、滑动、计时器、无障碍通知
命令面板cmdk过滤、键盘操作、分组、无障碍支持
动画数字NumberFlow数字列、本地化格式
数据表格TanStack Table(无头)排序、虚拟化、列状态
表单原生功能 + 小型验证库约束 API、将焦点移至首个错误

规则:如果 ARIA Authoring Practices 中有对应规范,就使用实现了该规范的基础组件。

状态矩阵

每个交互组件完成前都必须涵盖以下所有状态。写下来,并逐项核对。

状态视觉效果行为
默认——
悬停(仅限悬停设备)单级变化—
焦点可见焦点环—
激活 / 按下缩放至 .97 或变暗—
已选中 / 已勾选 / 已开启填充或强调色aria-* 反映该状态
已禁用50% 不透明度,不响应悬停不可聚焦;如果必须保留在 Tab 顺序中,则设置 aria-disabled
忙碌 / 加载中原位显示微调器,宽度不变忽略输入,设置 aria-busy
错误 / 无效边框 + 提示信息设置 aria-invalid,并通过 aria-describedby 指向提示信息
空(用于容器)一行说明 + 一个操作—
只读文本样式,无操作提示—

尺寸:两种或三种(sm、md、lg)。变体:primary、secondary(边框)、ghost、danger。不要七种,也不要 outline-gradient-xl。

根据状态设置样式,而非根据 props

样式读取元素的实际状态,使组件与其 CSS 不会出现不一致:

<button data-state={open ? "open" : "closed"} data-size={size} data-variant={variant} aria-busy={busy || undefined} />
.button[data-variant="primary"] { ... }
.button[data-state="open"] { ... }
.button[aria-busy] { ... }
.button:where(:disabled, [aria-disabled="true"]) { ... }

基础组件已经会提供 data-state、data-highlighted、data-disabled;直接基于这些属性设置样式,不要将它们复制到 React 状态中。

通过组合设计 API

由小部件组合而成,胜过一个拥有四十个 props 的组件:

<Dialog.Root>
  <Dialog.Trigger asChild><Button>Rename</Button></Dialog.Trigger>
  <Dialog.Content>
    <Dialog.Title>Rename file</Dialog.Title>
    <Dialog.Description>…</Dialog.Description>
    <Dialog.Close asChild><Button variant="secondary">Cancel</Button></Dialog.Close>
  </Dialog.Content>
</Dialog.Root>

指南:

  • 使用 asChild / render,将行为添加到用户的元素上,而不是再包一层。
  • 将其余 props 展开到根元素上;透传 className 和 style;转发 ref。
  • 状态使用布尔型 props,变体使用字符串联合类型;不要让 type="..." 与原生属性冲突。
  • 受控和非受控模式都要可用:使用 value + onValueChange,或 defaultValue。绝不能只实现其中一半。
  • 事件处理器命名为 onX,接收原生事件或普通值,绝不接收自行发明的合成对象。
  • 不要同时使用 isLoading / loading;统一使用一个名称,并保持一致。

使用 token,而非具体值

组件通过 token 引用颜色、圆角、间距、缓动和持续时间。如果组件中内联写有十六进制颜色、0.3s 或 cubic-bezier,就意味着它偏离了系统。

每一层的圆角保持一致:输入框和按钮为 6–8px,卡片和弹出框为 8–12px,对话框为 12–16px,药丸形元素使用全圆角。嵌套圆角逐层缩小:12px 卡片内的元素使用 8px,而不是 12px。

定位层级

弹出框、菜单和工具提示:使用 portal 挂载到 body,根据触发器定位并处理碰撞,将 transform-origin 设置为最终定位方向的起点,并以约 160ms 的缩放 / 透明度动画进入。对话框:居中显示,带背景遮罩,锁定滚动且不造成布局偏移(补偿滚动条宽度),打开时将焦点移入,关闭时将焦点移回。

Z-index 应使用层级刻度,而不是随意的数值:--z-sticky: 10; --z-dropdown: 20; --z-dialog: 30; --z-toast: 40。任何高于 50 的值都是错误。

输入框

  • 显示可见标签。占位符是提示,不是标签。
  • 错误消息显示在下方,并通过 aria-describedby 关联;在失焦或提交后显示,而不是每次按键都显示。
  • 高度与相同尺寸的按钮一致(sm 为 32px,md 为 36px,lg 为 40px)。
  • 触屏设备上使用 16px 字号,否则浏览器会缩放页面。
  • 设置 inputmode 和 autocomplete。手机上设置 enterkeyhint。
  • 输入框内的图标属于装饰元素,除非它们是按钮;即使按钮绘制为 16px,点击区域也应为 32px。

列表和行

  • 整行都是目标区域(在链接上使用覆盖整行的 ::after 伪元素),而不只是名称。
  • 只用一个悬停高亮,并随指针移动,不要为每行单独设置背景。
  • 键盘操作:按 j/k 或方向键移动,按 Enter 激活,聚焦的行要有清晰可见的轮廓。
  • 截断描述,不要截断名称。
  • 超过约 200 行时使用虚拟化。

图标

统一使用一套图标、一种描边宽度。在同一场景中统一尺寸(密集 UI 使用 14px,舒适场景使用 16px,触屏场景使用 20px)。使用 currentColor,使图标颜色与文本一致。装饰性图标使用 aria-hidden;仅含图标的按钮使用 aria-label。

表格

  • 表头行固定,并带有底部边框。
  • 数字右对齐并使用等宽数字;文本左对齐;不要居中。
  • 行高为 32–40px;仅在非常密集的表格中使用斑马纹,并采用 --hover 级别。
  • 在表头中显示排序指示器;无需悬停即可看出当前活动列。
  • 空状态应显示在表格区域内,而不是替代表格区域。

测试组件

在认为组件完成之前,打开它并:

  1. 拔掉鼠标,用 Tab 键遍历组件。
  2. 按下、按住、拖离,然后松开。
  3. 将窗口调整为 375px 宽。
  4. 切换深色和浅色模式。
  5. 开启减少动态效果设置。
  6. 分别填入最长的合理内容和最短的内容。
  7. 使用屏幕阅读器检查:组件是否有名称、角色和状态?

生成式组件的特征

特征修复方法
使用 useState 和外部点击钩子的自定义下拉菜单使用菜单基础组件
variant 有八种取值减为四种
未转发 className转发并合并
内联使用 transition: all 0.3s使用设计令牌
加载指示器替代标签,且按钮缩小将加载指示器覆盖显示,并固定宽度
仅含图标且没有标签的按钮使用 aria-label
div onClick改用 button
没有焦点陷阱的模态框使用对话框基础组件

目录中的资源

评论

支持 Markdown