mcpField
W
web-ui-radix-ui

web-ui-radix-ui

github.com/agents-inc设计与 UI2675 次安装2 天前更新
代码仓库

关于 web-ui-radix-ui

Radix UI 的无样式 React 基础组件指南,面向构建自定义界面的前端开发者。它重点说明复合组件、可访问性行为、元素组合及动画状态的使用方式。

主要功能

  1. 01无样式基础组件
  2. 02复合组件与共享上下文
  3. 03ARIA 与键盘交互支持
  4. 04asChild 元素组合
  5. 05data-state 动画钩子

适用场景

  1. 01构建对话框和弹出层
  2. 02实现菜单、选择器与提示框
  3. 03组合自定义按钮和触发元素
  4. 04为组件添加状态动画

安装

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

$npx skills add https://github.com/agents-inc/skills --skill web-ui-radix-ui

安装后在 Claude Code 中输入/web-ui-radix-ui确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
animation.md
core.md
forms.md
menus.md
navigation.md
overlays.md
preview.md

详细介绍

Radix UI 原语

**快速指南:**Radix 提供无样式原语,包含组件的行为和无障碍功能,但不包含外观。每个原语都是复合组件——Root、Trigger、Portal、Content——共享上下文;asChild 会将原语的行为合并到你自己的元素上;data-state 属性用于驱动 CSS 动画。当前版本为 v1.8.x:兼容 React 19 和 RSC,不再需要 forwardRef,而统一包 radix-ui 取代了各个 @radix-ui/react-* 包。

详细资源:


适用路径

  • 稳定原语 — 使用了八年的 API 表面,遵循语义化版本规范且稳定。请先参阅 examples/core.md,再参阅所需原语系列对应的文件。
  • 预览版原语 — unstable_OneTimePasswordField、unstable_PasswordToggleField、 unstable_Form。前缀就是 API 契约:这些原语会在次版本之间变更,因此请参阅 examples/preview.md,并做好升级时重新检查的准备。
  • 进入和退出动画 — 具体做法取决于复杂程度。基于 data-state 的 CSS @keyframes 可实现淡入淡出、滑动和高度动画;编排动画序列则需要 forceMount。两种做法均见于 examples/animation.md。

<critical_requirements>

编写 Radix UI 代码前

安装统一的 radix-ui 包,不要分别安装各个 @radix-ui/react-* 包。共享内部实现使用同一版本,可以避免重复的上下文提供程序以及各个原语之间的堆叠顺序冲突。

为覆盖层补全完整结构——Root、Trigger、Portal、Overlay、Content、Close。 每个部分都会向 Root 的上下文注册自身;缺少某个部分时,它所承载的行为也会消失,而不只是少了一段标记。

将覆盖层内容放在 Portal 中。 在原位置渲染的内容会继承所有祖先元素的 overflow: hidden 和堆叠上下文,因此,滚动面板内的对话框会被面板裁切。

为每个 Dialog 和 AlertDialog 提供 Title。 屏幕阅读器打开对话框时会播报此标题;而且从 v1.6.0 开始,Radix 在缺少标题时不再记录错误。

用于 asChild 的组件必须转发 ref,并展开所有 props。 Radix 通过 ref 定位浮动内容,并在关闭时恢复焦点;展开 props 则能让 ARIA 属性和事件处理程序传递到 DOM 元素。在 React 19 中,ref 是普通 prop;低于此版本时,需要用 forwardRef 包装组件。

</critical_requirements>


自动检测: radix-ui、@radix-ui、Dialog.Root、AlertDialog、DropdownMenu、Popover、Tooltip、 Accordion、Tabs、Select.Viewport、asChild、Slot、Slottable、ItemIndicator、data-state、forceMount、 onOpenAutoFocus、onInteractOutside、--radix-accordion-content-height、unstable_OneTimePasswordField、 unstable_PasswordToggleField、unstable_Form

适用范围:

  • 覆盖层行为——焦点陷阱、关闭、碰撞感知定位、堆叠顺序
  • 通过共享上下文协调各部分的复合组件 API
  • 菜单、选择器、标签页和手风琴的键盘交互与 ARIA 设置
  • 多态——通过 asChild 在不同元素上呈现原语的行为
  • 使用 Slot 构建支持 asChild 的自定义组件

其他部分另有说明:

  • 视觉设计——原语不带样式,并接受 className;如何生成该类名不属于本技能的范畴
  • 表单状态、架构验证和提交——Radix Select 或 Checkbox 只会报告其值,不提供其他功能
  • 图标设计——示例使用标记为 aria-hidden 的 Unicode 字形;是否提供实际图标需另行决定
  • JavaScript 中的动画编排——Radix 提供 forceMount 和 data-state,供检测组件是否存在的动画运行时使用

Radix 将组件通常紧密结合的两部分拆开。它负责困难且不可见的部分——ARIA 角色、焦点顺序、关闭语义、碰撞检测——而产品的独特性完全由你掌控。

这种拆分带来一个值得牢记的结论:原语由多个部分组成,而不是一个带 props 的组件。 Dialog 并不是一个供你配置的东西;它由 Root、Trigger、Portal、Overlay、Content、Title、Description 和 Close 组成,各部分通过你看不到的上下文进行通信。你可以按照设计要求,以所需的排列组合这些部分,并自动获得协同效果。有现成部分却还要寻找对应 prop,通常说明你正在和这种设计模式较劲。


<decision_framework>

选择原语

Blocking the page?
├─ Destructive confirmation → AlertDialog (no click-outside dismiss, by design)
├─ Form or content         → Dialog
└─ Page-level, not blocking → Dialog with modal={false}, or Popover

Floating beside a trigger?
├─ Opened by click, holds interactive content → Popover
├─ Opened by hover or focus, text only        → Tooltip
└─ A list of actions                          → DropdownMenu

Choosing from options?
├─ One value, in a form   → Select
├─ Several values         → Popover containing Checkbox items
└─ An action, not a value → DropdownMenu

Showing and hiding regions?
├─ One at a time, in place → Accordion (type="single" collapsible)
├─ Several at once         → Accordion (type="multiple")
└─ One panel replacing another → Tabs

</decision_framework>


核心模式

模式 1:复合组件结构

每个覆盖层都由相同的六个部分组成。Root 保存状态,Portal 脱离布局限制,Title 和 Description 提供可访问名称。

<Dialog.Root>
  <Dialog.Trigger>Open</Dialog.Trigger>
  <Dialog.Portal>
    <Dialog.Overlay className={overlayClass} />
    <Dialog.Content className={contentClass}>
      <Dialog.Title>Account settings</Dialog.Title>
      <Dialog.Description>Manage preferences and security.</Dialog.Description>
      <Dialog.Close aria-label="Close">×</Dialog.Close>
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

完整代码:examples/core.md

模式 2:受控状态与非受控状态

defaultOpen 让 Radix 管理状态。使用 open 和 onOpenChange 则由你接管状态——这在异步保存后关闭、从触发器以外的位置打开,或与 URL 保持同步时必不可少。 onOpenChange 是用户意图的唯一通道,因此省略它的受控原语永远无法关闭。

const [open, setOpen] = useState(false);

<Dialog.Root open={open} onOpenChange={setOpen}>
  {/* … */}
</Dialog.Root>;

完整代码:examples/overlays.md

模式 3:使用 asChild 实现多态

asChild 会指示组件部分渲染其子元素,而不是自身元素,并将属性和行为合并到子元素上。借助它,Tooltip.Trigger 可以变成锚点,或 Dialog.Trigger 可以变成你自己的按钮,而无需额外的包装元素。

<Tooltip.Trigger asChild>
  <a href="/docs">Documentation</a>
</Tooltip.Trigger>

此位置的自定义组件需要接收 ref 并展开其余属性,否则合并过程会悄无声息地丢弃 Radix 传递下来的处理程序和 ARIA 属性。

完整代码:examples/core.md

模式 4:使用 Slot 构建自己的 asChild 组件

Slot 是 asChild 使用的属性合并机制,也可以供你自己的组件使用。设置 asChild 时,将渲染的元素替换为 Slot.Root。当组件还会在子元素周围渲染固定装饰时,Slot.Slottable 用于标记子元素内容应放置的位置。

function Button({ asChild = false, ...props }: ButtonProps) {
  const Comp = asChild ? Slot.Root : "button";
  return <Comp {...props} />;
}

使用 Slot.Slottable,将 child={children} 和一个渲染函数作为其子元素,可以将子元素内容包装在你自己的标记中,同时由子元素保留合并后的属性。 Slot.createSlot("Button") 会返回一个 Slot,其错误消息会使用 Button 作为名称,并接受用于指定渲染元素及其属性的类型参数。

完整代码:examples/core.md

模式 5:Portal 及其容器

Portal 会将内容移至 document.body,使其脱离所有祖先元素的裁剪区域和层叠上下文。传入 container 可将内容移至其他位置,例如影子根、iframe 的 body,或微前端自己的挂载点。

<Popover.Portal container={containerRef.current}>
  <Popover.Content>
    <Popover.Arrow />
  </Popover.Content>
</Popover.Portal>

完整代码:examples/core.md

模式 6:通过 data-state 实现动画

Radix 会在可动画的部分设置 data-state="open" 和 "closed",并暂停卸载,直到退出动画报告已完成。它会监听 animationend,因此 @keyframes 会延迟卸载,而 transition 不会。

.dialog-overlay[data-state="open"] {
  animation: fadeIn 150ms ease-out;
}
.dialog-overlay[data-state="closed"] {
  animation: fadeOut 150ms ease-in;
}

对于由 JavaScript 运行时驱动的动画序列,Portal、Overlay 和 Content 上的 forceMount 会将卸载控制权交还给你。

完整代码:examples/animation.md

模式 7:焦点管理

模态内容会锁定焦点,并在关闭时将焦点返回触发器,无需任何配置。使用 onOpenAutoFocus 可更改初始焦点位置——破坏性操作的确认框应默认聚焦“取消”,而不是“删除”。

<AlertDialog.Content
  onOpenAutoFocus={(event) => {
    event.preventDefault();
    cancelRef.current?.focus();
  }}
>

完整代码:examples/overlays.md

模式 8:覆盖层的可访问名称

Title 是必需的可访问名称。如果设计中没有可见标题,请保留该元素并将其视觉隐藏——移除它会导致屏幕阅读器不再播报,而不只是移除文本。

<VisuallyHidden asChild>
  <Dialog.Title>Filter results</Dialog.Title>
</VisuallyHidden>

省略 Description 无需额外处理:自 v1.6.3 起,Radix 仅在渲染 Description 时才会设置 aria-describedby。

完整代码:examples/core.md


<red_flags>

风险提示

运行时会出问题:

  • 自定义的 asChild 子元素不接受 ref — Radix 无法测量它或将焦点设到它上面,因此浮层内容会定位错误,焦点也不会返回;将 ref 作为 prop 接受(React 19),或用 forwardRef 包装
  • 自定义的 asChild 子元素不传递其余 props — 合并后的事件处理器和 ARIA 属性会被丢弃,导致触发器看起来正常,却不起作用
  • Overlay 内容位于 Portal 外部 — 会被任何设置了 overflow: hidden 的祖先裁剪,并处于错误的层叠上下文中
  • Dialog 没有 Title — 缺少可访问名称,而且从 v1.6.0 起,不再有控制台错误提示这一问题
  • 用 transition 实现退出动画 — 卸载不会延迟,因此元素会在动画开始前消失;请改用 @keyframes
  • 使用 JavaScript 动画运行时挂载内容,却没有设置 forceMount — Radix 会在状态切换时立即卸载内容,导致退出动画刚到第一帧就被截断

容易引起意外的行为:

  • data-state 会在退出动画开始前切换为 "closed",而不是等动画结束后再切换
  • --radix-accordion-content-height 及其同类变量仅在动画运行期间设置,在动画之外读取时为空
  • AlertDialog 不支持点击外部或按 Escape 键关闭 — 只能通过 Cancel 或 Action 关闭,这正是选择它的原因
  • Slot 会合并处理器,而不是替换它们,并且会先运行子元素自身的处理器
  • 在通过 Portal 渲染的内容上设置显式 z-index 会与 Radix 维护的分层基础组件顺序冲突;应改为隔离容器
  • value={undefined} 的 Progress 表示不确定进度,而不是零
  • Select 和 ScrollArea 接受 nonce prop;当内容安全策略禁止未设置 nonce 的内联样式时,需要使用此属性

评论

支持 Markdown