首页
/ shadcn/ui 迁移指南:Radix UI 到 Base UI 的 progress、scroll-area、separator、avatar、toast、form Props 完整映射

shadcn/ui 迁移指南:Radix UI 到 Base UI 的 progress、scroll-area、separator、avatar、toast、form Props 完整映射

2026-09-04 13:07:22作者:姚月梅Lane

本文是 shadcn/ui 仓库内 Radix → Base UI 迁移技能包(skills/migrate-radix-to-base/)中「展示类与杂项原语」一族的 Props 映射参考的完整解读。它将逐组件、逐 Prop 地给出 Radix UI 与 Base UI(@base-ui/react,原 @base-ui-components/react)之间的部件映射、参数改名/删除清单、data 属性与 CSS 变量改写规则,并结合本仓库 registry 中现有的 Radix 组件源码,帮助你在实际项目中完成 progress、scroll-area、separator、avatar、toast、form 六个组件族的迁移,以及对 Label、AspectRatio、VisuallyHidden、AccessibleIcon 四个「无对应物」工具类原语的降级替换。

一、文档定位:迁移知识库中的 per-family props 表

本映射表位于 skills/migrate-radix-to-base/display-misc.md,它是 SKILL.md 定义的 Agent 迁移技能中「转换引擎(transformation engine)」的六份分族 props 参考表之一(其余为 overlays.mdmenus.mdform-controls.mddisclosure.mduniversal-patterns.md)。SKILL.md 明确要求:绝不猜测映射,表中没有的 Prop 必须去查 node_modules/@base-ui/react/**/*.d.ts,并把缺口记录进迁移报告

映射数据的来源在文档头部即有声明:Radix 侧取自 radix-ui 官网文档 data/primitives/docs/components/*.mdx 的完整内联 props 表,Base 侧取自 base-ui.com 各组件文档(抓取于 2026-07-02,对应 @base-ui/react 包)。姊妹文档 universal-patterns.md 补充说明:整库的知识基线是对 apps/v4/registry/bases/{radix,base}/ui/ 下 61 组组件对的机械 diff(first-party ground truth)加上 radix-ui@1.4.3 的包导出清单构建的,因此本文表格可直接作为迁移时的「查询词典」使用。

二、通用约定(适用于下文所有组件)

文档头部给出三条「通用约定」,对下文每一张表都成立,不再逐表重复:

  1. asChild(boolean)→ render,类型是 ReactElement | ((props: HTMLProps, state) => ReactElement)。注意签名变了,调用形态从「包裹子元素」变成「传渲染元素」:
// Radix
<Part asChild><a href="..." /></Part>

// Base UI
<Part render={<a href="..." />} />

对于手动 Slot 惯用法(const Comp = asChild ? Slot.Root : "a"),universal-patterns.md 给出了替代方案:useRender + mergeProps(来自 @base-ui/react/use-render / @base-ui/react/merge-props),并附了 breadcrumb-link 的完整 worked example;注意其中提到的坑:传入 mergeProps 的对象字面量里含 data-* 键时需要 as React.ComponentProps<"tag"> 断言,否则 tsc 报 excess-property 错误。

  1. Base UI 的 classNamestyle 都接受函数,参数是该部件的 State 对象。这是 data 属性改写之后「按状态写样式」的主力手段。

  2. 每个 Base 部件都暴露 Part.PropsPart.State 类型(例如 Progress.Root.Props)。迁移 shadcn 包装器时,类型标注从 React.ComponentProps<typeof XPrimitive.Root> 改为 XPrimitive.Root.Props

此外,通用模式表中与本文各组件直接相关的全局改写规则还有:data-[state=open]data-open(布尔存在式属性取代枚举值);进出场动画从 data-[state=open]:animate-in / data-[state=closed]:animate-out(keyframes)改写为 data-starting-style:* / data-ending-style:*(transition-based)。下文各组件的 data 属性表都是在这条全局规则之上的特化。

三、progress:Indicator 必须嵌套进新 Track,删除手动 translateX

3.1 部件映射

Progress.RootProgress.RootProgress.IndicatorProgress.Indicator,但 Indicator 现在必须嵌套在新的 Progress.Track。Base UI 新增 TrackLabelValue 三个部件。关键行为差异:Base UI 由原语自己计算 Indicator 的填充宽度(内联 style),因此 Radix 侧「在 Indicator 上写 style={{ transform: translateX(-(100 - value)%) }}」的惯用 hack 应删除,而不是搬运

这一条在本仓库中可以直接对证——现有 Radix 版包装器 apps/v4/registry/new-york-v4/ui/progress.tsx 正是这个 hack:

<ProgressPrimitive.Indicator
  data-slot="progress-indicator"
  className="h-full w-full flex-1 bg-primary transition-all"
  style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>

迁移到 Base UI 时,这行 style 连同对 value 的手动解构一起删除,宽度交由原语负责。

3.2 Progress.Root → Progress.Root

Radix prop 类型 / 默认 Base UI 对应 迁移说明
asChild boolean / false render 签名变更(见第二节)。
value number | null / - value 语义相同。Base UI 中为必填(默认 null)。两边 null 都表示 indeterminate。
max number / - max 相同。Base UI 默认 100;并新增 min(默认 0)。
getValueLabel (value: number, max: number) => string / - getAriaValueText 改名 + 签名变更:Base UI 是 (formattedValue: string | null, value: number | null) => string。百分比计算逻辑消失,格式化改用 format/locale

3.3 Progress.Indicator → Progress.Indicator

Radix prop 类型 / 默认 Base UI 对应 迁移说明
asChild boolean / false render 两侧唯一 Prop。嵌套进 Progress.Track;宽度由原语设置。

3.4 Base UI 独有、值得了解的 props

  • Root:min0)、formatIntl.NumberFormatOptions)、localeIntl.LocalesArgument)、aria-valuetext
  • 新部件:Progress.Track(包含 Indicator)、Progress.Label(无障碍标签,渲染 <span>)、Progress.Value(格式化值文本,渲染 <span>children 为渲染函数 (formattedValue, value) => ReactNode)。

3.5 Data 属性与 CSS 变量

Radix Base UI
[data-state="loading"] [data-progressing](布尔存在式属性取代枚举)
[data-state="complete"] [data-complete]
[data-state="indeterminate"] [data-indeterminate]
[data-value][data-max] 删除。需要时读 className/style 状态函数里的 value,或自行设置属性。

上述 Base 属性同时出现在 Root、Track、Indicator、Label、Value 上,State 类型为 { status: 'indeterminate' | 'progressing' | 'complete' }。CSS 变量:两侧均无。

四、scroll-area:Scrollbar/Thumb 改名,可见性从 props 变为 CSS 驱动

4.1 部件映射

ScrollArea.RootScrollArea.RootScrollArea.ViewportScrollArea.ViewportScrollAreaScrollbarScrollArea.ScrollbarScrollAreaThumbScrollArea.ThumbScrollArea.CornerScrollArea.Corner。Base UI 新增 ScrollArea.Content(包在 Viewport 内部,用于水平溢出测量)。本仓库现有实现 apps/v4/registry/new-york-v4/ui/scroll-area.tsx 展示了典型的 Radix 组装形态(Root > Viewport + 自定义 ScrollBar + Corner),迁移时保留该结构,仅重命名 Scrollbar/Thumb 两个部件。

4.2 ScrollArea.Root → ScrollArea.Root

Radix prop 类型 / 默认 Base UI 对应 迁移说明
asChild boolean / false render 签名变更。
type "auto" | "always" | "scroll" | "hover" / "hover" 删除 可见性改为 CSS 驱动:基于 [data-hovering]/[data-scrolling] 给 Scrollbar 写 opacity(对应 hover/scroll 行为),或写始终可见的 CSS 对应 "always"(配合 Scrollbar 的 keepMounted)。"auto" 是默认挂载行为(可滚动时才挂载 scrollbar)。
scrollHideDelay number / 600 删除 用 scrollbar opacity 过渡上的 CSS transition-delay 复现。
dir "ltr" | "rtl" / - 删除 Base UI 从 DOM(dir 属性)/ 其 DirectionProvider 工具读取方向;无组件级 prop。
nonce string / - 删除 无文档化的 CSP nonce 对应项。

4.3 其余部件

部件 映射 要点
ScrollArea.Viewport ScrollArea.Viewport asChildrender。角色相同(可滚动容器)。水平滚动重要时,把 children 包进 ScrollArea.Content
ScrollAreaScrollbar ScrollArea.Scrollbar asChildrenderforceMountkeepMounted(改名,boolean,默认 false,不可滚动时也保留在 DOM 中);orientation 相同、默认同为 "vertical"
ScrollAreaThumb ScrollArea.Thumb 两侧都只有 asChildrender
ScrollArea.Corner ScrollArea.Corner 同上。

Base UI 独有、值得了解的:Root 的 overflowEdgeThresholdnumber | Partial<{ xStart; xEnd; yStart; yEnd }>,默认 0),溢出边缘属性翻转的阈值;新部件 ScrollArea.Content(Viewport 内的 div,与 Root 相同的 overflow data 属性)。

4.4 Data 属性与 CSS 变量

Radix Base UI
Scrollbar [data-state="visible" | "hidden"] 删除。用 Scrollbar 上的 [data-hovering][data-scrolling][data-has-overflow-x/y] 驱动可见性样式。
Scrollbar/Thumb [data-orientation] 相同(Scrollbar 与 Thumb 上的 data-orientation)。
- 新增(Root/Content/Viewport/Scrollbar 上):data-has-overflow-xdata-has-overflow-ydata-overflow-x-start/enddata-overflow-y-start/enddata-scrolling;Scrollbar 另有 data-hovering

Radix 文档未列 CSS 变量(其实现带有未文档化的 --radix-scroll-area-thumb-*/corner-*)。Base UI 文档化的变量:

Base UI 变量 所在部件
--scroll-area-corner-width--scroll-area-corner-height Root
--scroll-area-thumb-width--scroll-area-thumb-height Scrollbar
--scroll-area-overflow-x-start/end--scroll-area-overflow-y-start/end Viewport(距各边缘的像素距离,非常适合做滚动渐隐遮罩)

五、separator:变为可调用单部件,decorative 删除

部件映射:Separator.RootSeparator可调用单部件,没有 .Root)。

Radix prop 类型 / 默认 Base UI 对应 迁移说明
asChild boolean / false render 签名变更。
orientation "horizontal" | "vertical" / "horizontal" orientation 相同,默认相同(Orientation 类型)。
decorative boolean / - 删除 Base UI 的 separator 恒为语义元素(role="separator")。纯视觉分隔线请渲染普通 <div aria-hidden="true"> 或直接用 CSS 边框。

Base UI 独有点:除通用 className/style/render 外无额外 prop,渲染 <div>。Data 属性:[data-orientation]horizontal | vertical)两侧一致。CSS 变量:两侧均无。

一个值得注意的仓库现状:现有包装器 apps/v4/registry/new-york-v4/ui/separator.tsx 默认传了 decorative = true。迁移时这一默认值要一并处理——Base 侧没有该开关,「默认纯装饰」的项目行为需要改为「默认语义分隔」,属于 SKILL.md 所说的「行为差异要标记(flag)、不得静默修补」的典型场景。

六、avatar:解剖结构不变,两处改名

部件映射:Avatar.RootAvatar.RootAvatar.ImageAvatar.ImageAvatar.FallbackAvatar.Fallback,解剖结构相同。Base 侧 Root 渲染 <span>、Image 渲染 <img>、Fallback 渲染 <span>

部件 Radix prop Base UI 对应 迁移说明
Root asChild render 两侧唯一 Prop。Base Root 也可以直接接受普通 children(例如首字母)而不使用 Image/Fallback。
Image asChild / onLoadingStatusChange render / onLoadingStatusChange 回调同名同类型("idle" | "loading" | "loaded" | "error"ImageLoadingStatus 联合)。
Fallback asChild / delayMs render / delay delayMs 改名为 delay,含义相同(显示 fallback 前等待的毫秒数)。

Base UI 独有点:除通用三件套外无新 prop。部件 State 暴露 imageLoadingStatus(Image 上还有 transitionStatus),供 className/style 函数使用。Data 属性:Radix 未文档化任何属性;Base UI Image 新增 data-starting-style / data-ending-style 用于进出场过渡。CSS 变量:两侧均无。仓库现有实现 apps/v4/registry/new-york-v4/ui/avatar.tsx 是「Radix 原语 + data-slot + 尺寸分组」的 shadcn 典型组装,迁移时仅需替换 import 与 asChild 用法,data-slot 命名与 className 体系可原样保留。

七、toast:心智模型整体切换——从声明式到 manager 驱动

这是六个组件族中变化最彻底的一个。Radix toast 是声明式的(你自己渲染 <Toast.Root open>);Base UI toast 是 manager 驱动的:toast 通过 Toast.useToastManager().add({ title, description, ... }) 命令式创建(或使用全局 Toast.createToastManager() 并传给 ProvidertoastManager),然后在 Viewport 内渲染 useToastManager().toasts.map((toast) => <Toast.Root key={toast.id} toast={toast} />)

7.1 部件映射

Radix 部件 Base UI 部件
Toast.Provider Toast.Provider(props 差异很大)
Toast.Viewport Toast.Portal + Toast.Viewport(Portal 为新增,默认追加到 <body>
Toast.Root Toast.Root(需要 toast 对象;通常包裹新增的 Toast.Content
Toast.Title Toast.Title(渲染 <h2>
Toast.Description Toast.Description(渲染 <p>
Toast.Action Toast.Action(按 toast 渲染;props 可来自 toast.actionProps
Toast.Close Toast.Close
- 新增:Toast.ContentToast.Positioner + Toast.Arrow(锚定式 toast)、Toast.createToastManagerToast.useToastManager

7.2 Toast.Provider → Toast.Provider

Radix prop 类型 / 默认 Base UI 对应 迁移说明
duration number / 5000 timeout 改名。默认值相同(50000 禁用自动消失)。按 toast 覆盖移到 add({ timeout })
label(必填) string / "Notification" 删除 Base UI 内部处理读屏播报;按 toast 的紧迫度用 add() 中的 priority: 'low' | 'high'
swipeDirection "right" | "left" | "up" | "down" / "right" 移走 现为 Toast.Root 上的 swipeDirection;接受单值或数组,默认 ['down', 'right']
swipeThreshold number / 50 删除 不可配置。用 data-base-ui-swipe-ignore 属性让元素退出 swipe。
announcerContainer Element | DocumentFragment / document.body 删除 最接近的对应是 Toast.Portalcontainer(决定 viewport 渲染位置)。

7.3 Toast.Viewport → Toast.Portal + Toast.Viewport

Radix prop 类型 / 默认 Base UI 对应 迁移说明
asChild boolean / false render Portal 与 Viewport 上均有。
hotkey string[] / ["F8"] 删除 Base UI 硬编码 F6 聚焦 viewport landmark,不可配置。
label string / "Notifications ({hotkey})" 删除 landmark 标签内部处理。

Base UI 独有:Portal.containerHTMLElement | ShadowRoot | ref | null)。

7.4 Toast.Root → Toast.Root(删除最多的表格)

Radix prop 类型 / 默认 Base UI 对应 迁移说明
asChild boolean / false render 签名变更。
type "foreground" | "background" / "foreground" add() 选项中的 priority 移走 + 改名:foregroundpriority: 'high'(紧急播报),background'low'(默认)。注意:Base UI 的 toast.type 是另一个概念(自由样式类别,如 'success',以 data-type 暴露)。
duration number / - add() 选项中的 timeout 移走 + 改名;按 toast 覆盖 Provider 的 timeout
defaultOpen boolean / true 删除 打开状态在 manager 中。add() 创建,close(id) 移除。
open boolean / - 删除 同上;不存在受控打开模式。add({ id }) 会就地 upsert 已存在的 toast。
onOpenChange (open: boolean) => void / - 删除(有替代) 用 toast 对象(add() 选项)中的 onClose / onRemove 回调。
onEscapeKeyDown (event: KeyboardEvent) => void / - 删除 焦点在 viewport 内时 Esc 关闭仍可用,但不可拦截。
onPause / onResume () => void / - 删除 定时器仍会在 hover、focus、window blur 时自动暂停,但没有回调。
onSwipeStart / onSwipeMove / onSwipeEnd / onSwipeCancel (event: SwipeEvent) => void / - 删除(有替代) Swipe 是「样式化而非脚本化」:[data-swiping][data-swipe-direction]--toast-swipe-movement-x/y 取代事件钩子。
forceMount boolean / - 删除 Root 从 toasts 数组渲染;退出动画在移除前得到 data-ending-style + toast.transitionStatus: 'ending'(之后触发 onRemove)。

Base UI 独有(Root 上):toast(必填的 Toast.Root.ToastObjectidtitledescriptiontypetimeoutpriorityupdateKeylimitedheightonCloseonRemoveactionPropspositionerPropsdata);swipeDirection(单值或数组)。

7.5 Title / Description / Action / Close

部件 映射 要点
Toast.Title Toast.Title asChildrender。Base 渲染 <h2>(Radix 渲染 <div>),要保留 div 就 render={<div />}。内容通常来自 toast.title
Toast.Description Toast.Description asChildrender。Base 渲染 <p>。内容通常来自 toast.description
Toast.Action Toast.Action asChildrenderaltText(必填)删除,无对应 prop——经 manager 创建 toast 时,把按钮的 props(含 handler 与 aria 属性)通过 add({ actionProps }) 传入。Base 独有 nativeButtonboolean,默认 truerender 非按钮时设 false)。
Toast.Close Toast.Close asChildrender;同样有 nativeButton

7.6 Base UI 独有 props、manager API、Data 属性与 CSS 变量

  • Provider:limitnumber,默认 3;超出的 toast 会被加 data-limited + inert,而不是被移除)、toastManager(来自 Toast.createToastManager(),供 React 外部使用)。
  • Manager API(useToastManager() 返回值 / createToastManager()):toastsadd(options) => idclose(id?)update(id, options)promise(promise, { loading, success, error })
  • 新部件:Toast.Content(堆栈折叠时裁剪溢出;data-behinddata-expanded)、Toast.Positioner/Toast.Arrow(锚定式 toast,完整 popup 定位面:anchorside 默认 'top'alignsideOffsetalignOffsetcollisionAvoidancecollisionBoundarycollisionPaddingarrowPaddingstickypositionMethoddisableAnchorTracking)。

Data 属性对照:

Radix Base UI
Root [data-state="open" | "closed"] data-starting-style / data-ending-style(CSS 过渡钩子)
Root [data-swipe="start" | "move" | "cancel" | "end"] 滑动中为 [data-swiping]"end"[data-ending-style][data-swipe-direction=...]
Root [data-swipe-direction]up/down/left/right 同名同值
- 新增:Root 的 data-expandeddata-limiteddata-type;Viewport 的 data-expanded;Content 的 data-behinddata-expanded;Title/Description/Close/Action 的 data-type;Positioner/Arrow 的 data-sidedata-aligndata-anchor-hidden/data-uncentered

CSS 变量对照:

Radix Base UI
--radix-toast-swipe-move-x / --radix-toast-swipe-move-y --toast-swipe-movement-x / --toast-swipe-movement-y
--radix-toast-swipe-end-x / --radix-toast-swipe-end-y 删除;基于 [data-ending-style][data-swipe-direction=...] + movement 变量做消失动画
- Root 新增:--toast-index--toast-offset-y--toast-height;Viewport:--toast-frontmost-height;Positioner:--anchor-width/height--available-width/height--transform-origin

一个来自 universal-patterns.md 的覆盖矩阵注记:Toast 不属于 registry 的「golden pair」组件对(shadcn 用户大多直接用 sonner),因此其规格完全依据 Base 文档编写——迁移时若项目中实际使用 sonner,按 SKILL.md 的硬规则应原样保留并在报告中列为「有意不迁移」。

八、form:Radix Form 拆成 Form / Field / Fieldset 三件套

Base UI 把 Radix Form 拆到三个组件:Form@base-ui/react/form,可调用单部件,渲染 <form>)、Field@base-ui/react/fieldRootLabelControlErrorDescriptionValidityItem)、Fieldset@base-ui/react/fieldsetRootLegend)。

8.1 部件映射

Radix 部件 Base UI 部件
Form.Root Form(可调用,无 .Root
Form.Field Field.Root
Form.Label Field.Label
Form.Control Field.Control(或任意 Base UI 输入组件:Input、Checkbox、Select 等开箱即用即可放入 Field)
Form.Message Field.Error(校验错误);纯提示文字用 Field.Description
Form.ValidityState Field.Validity
Form.Submit 删除;用普通 <button type="submit">
- 新增:Fieldset.Root + Fieldset.LegendField.Item(checkbox/radio 组中逐项包装)

8.2 Form.Root → Form

Radix prop 类型 / 默认 Base UI 对应 迁移说明
asChild boolean / false render 签名变更。
onClearServerErrors () => void / - 删除(有替代) 服务端错误模型变化:向 Form 传入 errors 对象(key 为 Field.Rootname,value 为消息),并在 onFormSubmit(Base 会替你 preventDefault())或逐字段 onValueChange 里自行清除错误状态。

Base UI 独有(Form 上):errorsErrors)、onFormSubmit(formValues, eventDetails) => void)、validationMode'onSubmit' | 'onBlur' | 'onChange',默认 'onSubmit')、actionsRef{ validate(fieldName?) })。

8.3 Form.Field → Field.Root

Radix prop 类型 / 默认 Base UI 对应 迁移说明
asChild boolean / false render 签名变更。
name(必填) string / - name 目的相同(提交标识 + 匹配 Form errors 的 key);Base UI 中为可选,且优先于 Field.Control 上的 name
serverInvalid boolean / - 删除(有替代) 二选一:通过 Formerrors={{ [name]: message }} 提供消息(字段变为 invalid,Field.Error 展示它),或用 Field.Rootinvalid 布尔 prop 强制状态。

Base UI 独有(Field.Root 上):validate(value, formValues) => string | string[] | Promise<...> | null,取代 Radix 函数式 match 的自定义校验)、validationModevalidationDebounceTime0)、disabledinvaliddirtytouchedactionsRef{ validate() })。

8.4 Label / Control / Message

映射 要点
Form.LabelField.Label asChildrender;与控件的自动关联保留。Base 独有 nativeLabelboolean,默认 truerender 换成非 label 元素时设 false,例如用 <div><Select.Trigger> 按钮打标签)。
Form.ControlField.Control asChildrender。复合控件可以完全跳过 Control:Base UI 输入组件(Input、Checkbox、Select…)直接接入 Field.Root,这是 Radix Form 做不到的。Base 独有 defaultValuestring | number | string[])、onValueChange(value, eventDetails) => void)。
Form.MessageField.Error 见下表。

Form.Messagematch 是本表最需要逐字段核对的一项:

Radix prop 类型 / 默认 Base UI 对应 迁移说明
asChild boolean / false render 签名变更。
match 'badInput' | 'patternMismatch' | 'rangeOverflow' | 'rangeUnderflow' | 'stepMismatch' | 'tooLong' | 'tooShort' | 'typeMismatch' | 'valid' | 'valueMissing' | ((value, formData) => boolean | Promise<boolean>) / - match 签名变更:Base UI 为 boolean | 'valid' | 'badInput' | 'customError' | 'patternMismatch' | 'rangeOverflow' | 'rangeUnderflow' | 'stepMismatch' | 'tooLong' | 'tooShort' | 'typeMismatch' | 'valueMissing'函数形式消失:自定义规则移到 Field.Rootvalidate(返回错误字符串),无 match 的 Error 组件随即展示它们;'customError' 匹配 validate 失败。
forceMatch boolean / false match={true} 改名/吸收:match 接受 true 时始终显示消息(对外部库与服务端错误的文档化挂点)。
name string / - 删除 Field.Error 不能从外部指向字段;必须嵌套在所属的 Field.Root 内。

补充说明:Radix 在 children 省略时会按 match 渲染默认英文消息;Base UI 渲染的是来自 validate/Form errors 的错误字符串,否则请提供 childrenField.Description(仅 className/style/render)是非错误提示文字的新家。Error 渲染 <div>,Description 渲染 <p>

8.5 ValidityState、Submit 与新 Fieldset

| Form.ValidityState | 映射到 Field.Validitychildren 为必填渲染函数,签名变为 (state: Field.Validity.State) => React.ReactNode,原生标志位于 state.validity.*(另有 state.errorsstate.errorstate.valuestate.initialValue);Radix 的 name prop 删除,必须嵌套在 Field.Root 内。 | | Form.Submit | 无对应部件;渲染普通 <button type="submit">(或样式化 Button 组件)。 | | 新增 Fieldset | Fieldset.Root 渲染原生 <fieldset>(props:className/style/render;state { disabled }data-disabled)。Fieldset.Legend 渲染 <div> 并自动关联为无障碍 legend。用于把相关字段归在一个标签下。 |

8.6 表单级 Base UI 独有能力、Data 属性与 CSS 变量

  • 校验时机可配置(Form 或逐 Field 的 validationModevalidationDebounceTime);
  • Form 与 Field.Root 都有 actionsRef 命令式 validate()
  • Field.Item 把组内单个 checkbox/radio 与其自身 label/description 分组(disabled prop);
  • 提交时焦点移至第一个无效字段,与 Radix 行为一致。

Data 属性对照:

Radix(Field/Label/Control/Message) Base UI(全部 Field 部件:Root、Item、Label、Control、Description、Error)
[data-valid] [data-valid](相同)
[data-invalid] [data-invalid](相同)
- 新增:data-dirtydata-toucheddata-filleddata-focuseddata-disabled;Error 另得 data-starting-style/data-ending-style

CSS 变量:两侧均无。

九、无 Base UI 对应物的 Radix 工具:Label / AspectRatio / VisuallyHidden / AccessibleIcon

文档最后一节列出四个「无对应物」原语及其推荐的纯 CSS/原生替代——这些与 SKILL.md 的硬规则(「No Base UI counterpart」清单)完全一致:

  • Label(Radix Label.RootasChildhtmlFor):用原生 <label htmlFor="...">;在 Field.Root 内则用 Field.Label(自动建立关联,无需 htmlFor)。Radix 唯一的行为增强(防止双击选中文字)一行 CSS 即可:select-none / user-select: none
  • AspectRatio(Radix AspectRatio.RootasChildratio 默认 1):用 CSS aspect-ratio 属性——该 prop 本就映射到它。仓库现有实现 apps/v4/registry/new-york-v4/ui/aspect-ratio.tsx 只是一个透传 Radix 原语的空壳,印证了这一点:ratio={16 / 9}aspect-videoaspect-[16/9]aspect-ratio: 16 / 9),媒体子元素加 w-fullobject-cover
  • VisuallyHidden(Radix VisuallyHidden.RootasChild):在 <span> 上用 Tailwind 的 sr-only 类(标准 clip-rect 模式)。注意其他文件里一些 Base UI popup 组件仍需要隐藏标题满足 a11y,<span className="sr-only"> 同样覆盖该场景。
  • AccessibleIcon(Radix AccessibleIcon.Rootlabel 必填):它只是 VisuallyHidden + aria-hidden 的组合:给图标渲染 aria-hidden="true"(或 focusable="false")并在旁边加 <span className="sr-only">{label}</span>,或者干脆把 aria-label={label} 放到可交互的父级(button/link)上。

十、落到实处的迁移要点

把本文当参考词典后,执行层面遵循技能包既有纪律(均来自 SKILL.md):

  1. Preflight:先跑 npx shadcn@latest info --json 确认当前 base/风格/Tailwind 版本,用项目自己的包管理器安装依赖,保持 git 工作区干净,每个组件一个 commit,并在动依赖前先跑基线 typecheck/build。
  2. @base-ui/react 与 radix 并存安装,只有最后一个 Radix 组件迁移完成后才移除 Radix 依赖。
  3. shadcn 风格的包装器优先走「golden pair」:整库模式下用 shadcn add <component> --overwrite 直接交付 base 变体;渐进模式下把 base 变体写到 <component>-base.tsx(绝不动原文件),消费者逐个改 import 后再删除旧文件。本文表格主要服务于「转换引擎」路径——手写 Radix 代码、非 shadcn 项目、legacy 风格(new-york/new-york-v4/default 没有 base-new-york 对应风格,只允许分类检测、只重连原语、保留用户自己的 class)。
  4. 行为差异只标记、不静默修补:toast 的 hotkey 从可配置 F8 变为硬编码 F6、Separator 默认语义化、Form 焦点行为等,都应在 .migration/<component>.md 报告的「Behavior changes」段落中列出。
  5. 收尾自检:逐文件 typecheck、逐批 build、最后与基线对比全量 build;对本文涉及的每个文件跑残留扫描 grep -n "radix-ui\|@radix-ui",确认零命中后才算迁移完成。

按以上结构执行,progress/scroll-area/separator/avatar 属于低风险直映(改 import、改名、换 render),toast 需要重写状态管理模型,form 需要按三件套重组——六族的每一个 Prop 级决策,现在都可以在 skills/migrate-radix-to-base/display-misc.md 与本仓库 registry 源码之间找到对应依据。

登录后查看全文
热门项目推荐
相关项目推荐