首页
/ shadcn/ui 迁移指南:Radix UI 到 Base UI 表单控件(Select、Checkbox、Radio、Switch、Slider)的 Props 对照与实战

shadcn/ui 迁移指南:Radix UI 到 Base UI 表单控件(Select、Checkbox、Radio、Switch、Slider)的 Props 对照与实战

2026-09-05 10:52:25作者:裘旻烁

本篇以 skills/migrate-radix-to-base/form-controls.md 这份迁移参考文档为主体,系统讲解 shadcn/ui 项目中 Radix UI 五大表单控件(select、checkbox、radio-group、switch、slider)向 Base UI 迁移时的 Part 结构拆分、Props 签名变化、data 属性与 CSS 变量改写规则,并结合本仓库 base 注册表中的真实包装组件源码(select.tsxslider.tsx 等)验证目标形态。读完本文,你可以独立完成表单控件家族的 Radix→Base UI 迁移:改对 import、改对 Part 嵌套结构、改对回调签名,并知道哪些行为差异必须显式决策而不是静默修补。

原文档的适用范围是 select、checkbox、radio-group、switch、slider 五个原语,信息来源为 radix-ui.com 与 base-ui.com 的组件文档(文档注明抓取时间为 2026-07-02),配套的消费侧调用点变更表见 consumer-props.md,整体迁移策略(golden pair、渐进式/整站迁移、报告规范)见 SKILL.md

一、全局约定:每个组件都要先改的四件事

在逐个组件对照之前,原文档给出四条适用于所有 Part 的全局约定。它们决定了迁移的代码改动面,也解释了为什么“只改 import”几乎总是编译不过。

  1. asChildrender。Radix 的 asChildboolean,默认 false)对应 Base UI 的 render(类型 ReactElement | ((props, state) => ReactElement))。Base UI 的每个 Part 都接受 render,并且 className/style 既可以是普通值,也可以是状态回调 (state) => ...。这是 consumer-props.md 中唯一一条“Universal”级变更:<Trigger asChild><Button/></Trigger> 必须写成 <Trigger render={<Button/>}>...
  2. nativeButton 是 Base UI 独有。渲染交互式元素的 Base UI Part 接受 nativeButton,用于告诉 Base UI 你的 render 目标是不是原生 <button>——Radix 没有对应物。默认值因 Part 而异(Select.Trigger 默认为 true,Select.Item、Checkbox.Root、Switch.Root 等默认为 false),渲染非 button 节点时要显式设置。
  3. 回调追加 eventDetails 第二参数。回调会带上 { reason, event, cancel(), allowPropagation(), isCanceled, isPropagationAllowed, trigger }。其中 eventDetails.cancel() 取代了 Radix 里 event.preventDefault() 拦截默认组件行为的写法。注意类型层面的兼容红利:consumer-props.md 的“Callback signature rule”指出,Base UI 回调新增的 event-details 参数使得旧的单参 handler 仍然类型安全,但原来依赖 Radix 事件参数的 handler 必须逐条核对。
  4. data-state="x" 拆成 presence 属性。Radix 的单一 data-state token 在 Base UI 里变成 data-checkeddata-uncheckeddata-open 等存在性属性;Base UI 还普遍附加 Field 集成属性(data-validdata-invaliddata-dirtydata-toucheddata-filleddata-focused)和动画钩子(data-starting-styledata-ending-style)。同时 Radix 的 dir prop 没有逐组件等价物,方向统一由 DirectionProvider(或祖先元素的 dir HTML 属性)决定——注意 SKILL.md 提醒的是 direction prop 而非 dir

这四条约定意味着:类名里写死了 data-[state=open] 这类选择器的样式,必须按各组件的 Data-attribute 对照表(后文分节给出)重写;[consumer-props.md](https://gitcode.com/GitHub_Trending/ui/ui/blob/ee628d75dea87325735fafa7c54f5d7d7edb8774/skills/migrate-radix-to-base/consumer-props.md?utm_source=gitcode_repo_files)class-mapping.md 的 data 属性/CSS 变量改写规则是迁移引擎的强制步骤。

二、Select:Part 拆分最多、映射最复杂的组件

2.1 Part 级映射

Select 的 Part 映射关系是:Root -> RootTrigger -> TriggerValue -> ValueIcon -> IconPortal -> PortalContent -> Portal > Positioner > Popup(一个 Part 拆成三层)Viewport -> ListItem -> ItemItemText -> ItemTextItemIndicator -> ItemIndicatorScrollUpButton -> ScrollUpArrowScrollDownButton -> ScrollDownArrowGroup -> GroupLabel -> GroupLabelSeparator -> SeparatorArrow -> Arrow

其中两个易错点值得单独强调:

  • Radix 的 Select.Content 一个 Part 同时负责定位、碰撞处理和面板;Base UI 中定位属性放在 Positioner 上,面板/焦点属性放在 Popup 上,关闭拦截则上移到 Root.onOpenChange 的 eventDetails 里。
  • Radix 的 Select.Label分组标题,对应 Base UI 的 Select.GroupLabel;Base UI 新设的 Select.Label 是一个全新 Part,用于给 Trigger 本身加标签(渲染在弹窗外部)。迁移时若把 Radix Label 误映射到 Base UI Label,语义就错了。

本仓库 base 注册表的 select.tsx 正是这个目标形态的权威参照:SelectContent 包装组件内部就是 Portal > Positioner > Popup 三层结构,Positioner 上承接 sidesideOffsetalignalignOffsetalignItemWithTrigger 五个定位属性,Popup 内按 ScrollUpButton > List > ScrollDownButton 顺序嵌套子项;SelectLabel 包装组件则直接使用 SelectPrimitive.GroupLabel(见 select.tsx L66-L106L108-L119)。

2.2 Select.Root

Radix prop 类型 / 默认 Base UI 等价 迁移说明
defaultValue string,无默认 defaultValue: Value[] | Value | null 同名,类型放宽:值可为任意类型(支持对象),multiple 时取数组。
value string,无默认 value: Value[] | Value | null 同名,类型放宽;null 表示“无值”(显示 placeholder)。
onValueChange (value: string) => void (value, eventDetails) => void 签名变化:第二参数 eventDetailsreason'trigger-press' | 'outside-press' | 'escape-key' | 'window-resize' | 'item-press' | 'focus-out' | 'list-navigation' | 'cancel-open' | 'none')和 cancel()
defaultOpen boolean,无默认 defaultOpen: boolean,默认 false 相同。
open boolean,无默认 open: boolean 相同。
onOpenChange (open: boolean) => void (open, eventDetails) => void Radix Content 的 onEscapeKeyDown/onPointerDownOutside 拦截逻辑上移到这里:检查 eventDetails.reason === 'escape-key' / 'outside-press',调用 eventDetails.cancel() 阻止关闭。
dir "ltr" | "rtl" 移除 改用 DirectionProvider 或祖先的 dir 属性。
name string name: string 相同(Base UI 渲染一个隐藏 <input>)。
disabled boolean disabled: boolean,默认 false 相同。
required boolean required: boolean,默认 false 相同。

Base UI 的 Select.Root 不渲染 HTML 元素(Radix Root 同样如此)。

2.3 Select.Trigger 与 Select.Value

Radix prop Base UI 等价 迁移说明
asChild render Base UI Trigger 默认渲染 <button>,此处 nativeButton 默认 true;用 render 渲染非 button 时要置 false
(无) disabled: boolean Base UI 允许单独禁用 Trigger。

Select.Value 侧:asChildrenderplaceholder 同名保留,但行为有变化:Radix 的 Value 渲染所选 Item 的 ItemText 内容,Base UI 渲染的是原始值字符串,除非你在 Root 上传 items 或用 children 函数 (value) => ReactNode 格式化。如果你的 item 标签与 value 不同,必须在 Root 提供 items 或通过 children 格式化。consumer-props.md 也对应给出调用点动作:useState<string> + onValueChange={setState} 会因 value 类型放宽为 Value | null 而断型,需把 state 放宽为 string \| null 或包一层 setter。

2.4 Select.Content → Portal + Positioner + Popup

Radix prop Base UI 等价 迁移说明
asChild render(在 Positioner 和/或 Popup 上) 同模式。
position"item-aligned" | "popper",默认 "item-aligned" Positioner 的 alignItemWithTrigger: boolean,默认 true 枚举变布尔:"item-aligned"alignItemWithTrigger(默认),"popper"alignItemWithTrigger={false}。空间不足或触摸输入时 Base UI 会自动禁用该模式。
side(默认 "bottom" Positioner side: Side,默认 'bottom' 移过去;Base UI 新增 'inline-start' | 'inline-end' 逻辑值。仅在 alignItemWithTrigger 关闭时生效(对应 Radix 的 popper 模式)。
sideOffset(默认 0 Positioner sideOffset: number | OffsetFunction,默认 0 移过去,类型放宽(可传 { side, align, anchor, positioner } 的函数)。
align(默认 "start" Positioner align: Align,默认 'center' 默认值不同:Radix 是 "start",Base UI 是 'center'。要保留 Radix 表现请显式传 align="start"
alignOffset(默认 0 Positioner alignOffset: number | OffsetFunction 移过去,类型放宽。
avoidCollisionsboolean,默认 true Positioner collisionAvoidance: CollisionAvoidance 布尔变配置对象:{ side: 'flip' | 'shift' | 'none', align: 'flip' | 'shift' | 'none', fallbackAxisSide: 'start' | 'end' | 'none' }avoidCollisions={false}collisionAvoidance={{ side: 'none', align: 'none', fallbackAxisSide: 'none' }}
collisionBoundary(默认视口) Positioner collisionBoundary: Boundary,默认 'clipping-ancestors' 默认值不同:Radix 默认视口,Base UI 默认裁剪祖先。
collisionPadding(默认 10 Positioner collisionPadding: Padding,默认 5 移过去,默认值变化(10 → 5)。
arrowPadding(默认 0 Positioner arrowPadding: number,默认 5 移过去,默认值变化(0 → 5)。
sticky"partial" | "always",默认 "partial" Positioner sticky: boolean,默认 false 签名与语义都不同:Base UI sticky 表示锚点滚出视口后弹窗留在视口内,无 "always" 等价物。
hideWhenDetached(默认 false 移除(用 workaround) 无对应 prop:监听 Positioner 上的 data-anchor-hidden(锚点被隐藏时存在),例如 [data-anchor-hidden] { visibility: hidden }
onCloseAutoFocus Popup 的 finalFocus 不再在事件 handler 里 preventDefault,改为传 finalFocusboolean | RefObject | ((closeType) => boolean | void | HTMLElement | null)false 即“不移焦点”(等价 preventDefault()),传 ref/元素即聚焦到该元素。
onEscapeKeyDown 上移到 Root.onOpenChange 检查 eventDetails.reason === 'escape-key'eventDetails.cancel() 阻止关闭。
onPointerDownOutside 上移到 Root.onOpenChange 检查 eventDetails.reason === 'outside-press'eventDetails.cancel() 阻止关闭。

consumer-props.md 对应调用点一行:position="popper" | "item-aligned" → Positioner 上的 alignItemWithTrigger 布尔(wrapper 会把它透出),"popper"alignItemWithTrigger={false}

2.5 其余 Part:Viewport、Item、ItemText、ItemIndicator、滚动箭头、Group、Label、Separator、Arrow

Radix Part Base UI Part 关键迁移说明
Viewport List(改名) 两侧除 asChildrender 外无其他 prop。结构变化:Radix 要求 ScrollUpButton/Viewport/ScrollDownButton 作为 Content 的兄弟;Base UI 中 List 与滚动箭头是 Popup 的子元素。
Item Item asChildrender,另有 nativeButton(默认 false,Base UI Item 渲染 <div>)。value 由必填 string 放宽为 any(允许对象,配合 Root 的 isItemEqualToValue/itemToString*);null value 标记 placeholder 项。textValuelabel(改名,同样驱动 typeahead 文本匹配,默认取 item 文本内容)。
ItemText ItemText asChildrender;注意元素变化:Radix 渲染 <span>,Base UI 渲染 <div>
ItemIndicator ItemIndicator Base UI 默认未选中即卸载(与 Radix 一致);新增 keepMounted: boolean 可保留在 DOM(Radix 的 select ItemIndicator 上没有 forceMount)。
ScrollUpButton / ScrollDownButton ScrollUpArrow / ScrollDownArrow(改名) 新增 keepMounted: boolean(默认 false),可在弹窗不可滚动时保留箭头在 DOM;Base UI 的箭头在触摸输入下不渲染。
Group Group asChildrender
Label GroupLabel(改名) 不要映射到 Base UI Select.Label——那是给 trigger 本身加标签的新 Part(渲染在弹窗外)。
Separator Separator Base UI 新增 orientation: Orientation,默认 'horizontal'
Arrow Arrow width(默认 10)与 height(默认 5)被移除:用 CSS 控制尺寸,Base UI Arrow 渲染一个由你自己填 SVG 的 <div>

2.6 Base UI 独有、值得了解的 Select 能力

  • Root.multiple: boolean(默认 false):多选,值为 Value[],Radix 无对应。
  • Root.itemsRecord<string, ReactNode> | { label, value }[] | Group[],让 Select.Value 渲染标签而非原始值。
  • Root.isItemEqualToValueRoot.itemToStringLabelRoot.itemToStringValue:对象值支持。
  • Root.modal: boolean(默认 true):滚动锁定 + 阻挡外部指针;Radix select 一直是模态式的,要非模态行为需显式 modal={false}
  • Root.readOnlyRoot.autoCompleteRoot.formRoot.inputRefRoot.idRoot.onOpenChangeCompleteRoot.actionsRef(提供 { unmount() } 用于外部控制的退出动画)、Root.highlightItemOnHover(默认 true)。
  • 新 Part:Select.Backdrop(弹窗下的遮罩层)、Select.Label(trigger 标签)、Popup.finalFocus
  • Positioner.anchorPositioner.positionMethod'absolute' | 'fixed')、Positioner.disableAnchorTracking

2.7 Select 的 data 属性与 CSS 变量对照

Data 属性:

Radix Base UI
Trigger data-state="open" | "closed" Trigger data-popup-open(presence),另有 data-presseddata-popup-side
Trigger data-placeholder Trigger/Value data-placeholder(相同)
Trigger data-disabled Trigger data-disabled(相同),另有 data-readonlydata-required 及 Field 属性
Content data-state="open" | "closed" Positioner/Popup data-open / data-closed(presence)
Content data-sideleft/right/bottom/top Positioner/Popup data-sidenone/top/bottom/left/right/inline-start/inline-end
Content data-align Positioner/Popup data-align(取值相同)
Item data-state="checked" | "unchecked" Item data-selected(presence,无 unchecked token)
Item data-highlighted Item data-highlighted(相同)
Item data-disabled Item data-disabled(相同)
(无) Popup/Backdrop/ItemIndicator/ScrollArrows 的 data-starting-style / data-ending-style(动画钩子)
(无) Positioner data-anchor-hidden、ScrollArrows data-direction / data-visible

CSS 变量:Base UI 的变量全部设在 Select.Positioner 上(Radix 只设在 Content 上且仅 popper 模式有效):

Radix Base UI
--radix-select-trigger-width --anchor-width
--radix-select-trigger-height --anchor-height
--radix-select-content-available-width --available-width
--radix-select-content-available-height --available-height
--radix-select-content-transform-origin --transform-origin

select.tsx 包装组件的类名正体现了这套变量与 presence 属性:Popup 上写 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) 并配合 data-[align-trigger=true]:animate-none,与上表一一对应。

三、Checkbox:checked 拆分出 indeterminate

Part 映射:Root -> RootIndicator -> Indicator。元素变化:Radix Root 渲染 <button>(表单内含隐藏 input);Base UI Root 始终渲染 <span> 加隐藏 <input>(要用 nativeButton + render 才渲染真正的 button)。本仓库 base 注册表 checkbox.tsx 即此形态:CheckboxPrimitive.Root 直接承载 data-slot 与类名,内部只挂 CheckboxPrimitive.Indicator

3.1 Checkbox.Root

Radix prop Base UI 等价 迁移说明
asChild render 渲染 <button> 时配合 nativeButton
defaultCheckedboolean | 'indeterminate' defaultChecked: boolean,默认 false 签名变化'indeterminate' 不再是 checked 的取值,改用独立的 indeterminate: boolean prop。
checkedboolean | 'indeterminate' checked: boolean + indeterminate: boolean 拆成两个 prop:Radix checked="indeterminate" → Base UI indeterminate(半选与勾选/未勾选相互独立)。
onCheckedChange(checked: boolean | 'indeterminate') => void (checked: boolean, eventDetails) => void checked 恒为布尔,新增 eventDetailsreason: 'none');半选状态迁移由你通过 indeterminate prop 管理(或在 CheckboxGroup 中由 parent 管理)。
disabled disabled: boolean,默认 false 相同。
required required: boolean,默认 false 相同。
name name: string 相同。
value(Radix 默认 "on" value: string 同名。Radix 文档标默认 "on";Base UI 文档未标默认,但隐藏 input 提交行为与原生 checkbox 一致(未设置时提交 "on")。

3.2 Checkbox.Indicator 与 Base UI 独有能力

  • asChildrenderforceMountkeepMounted: boolean(默认 false)。两者都用于未勾选时保留元素在 DOM(配合动画);Base UI 在 indeterminate 时也会渲染 indicator。
  • Base UI 独有:Root.indeterminate(与 checked 解耦的混合态);Root.parent: boolean + 全新组件 CheckboxGroupvalue/defaultValue/onValueChange(string[], eventDetails)/allValues/disabled)——为一组 checkbox 提供共享状态,支持父级“全选”checkbox,Radix 无等价物;以及 Root.readOnlyRoot.uncheckedValue(未勾选时提交的值)、Root.formRoot.inputRefRoot.idRoot.nativeButton

Data 属性对照:data-state="checked"data-checkeddata-state="unchecked"data-uncheckeddata-state="indeterminate"data-indeterminatedata-disabled 保持不变;Base UI 新增 data-readonlydata-required 及 Field 属性(data-validdata-invaliddata-dirtydata-toucheddata-filleddata-focused);Indicator 上有 data-starting-style / data-ending-style。两侧均无 CSS 变量。

consumer-props.md 的调用点结论一句话可概括:checked="indeterminate"indeterminate + 布尔 checked

四、Radio Group:命名空间重组

Part 映射是本文档中结构调整最大的一处:Radix 只有一个 RadioGroup 命名空间;Base UI 拆成 RadioGroup(单一组件,无子 Part)和 RadioRadio.RootRadio.Indicator)。具体为:RadioGroup.Root -> RadioGroupRadioGroup.Item -> Radio.RootRadioGroup.Indicator -> Radio.Indicator。元素变化:Radix Item 渲染 <button>;Base UI Radio.Root 渲染 <span> 加隐藏 <input>nativeButton + render 可渲染真 button)。仓库中 radio-group.tsx 的包装结构与之完全一致:RadioGroup 包装 RadioGroupPrimitive(来自 @base-ui/react/radio-group),RadioGroupItem 包装 RadioPrimitive.Root(来自 @base-ui/react/radio),indicator 内放一个 cn-radio-group-indicator-icon 的 span。

4.1 RadioGroup.Root → RadioGroup

Radix prop Base UI 等价 迁移说明
asChild render 同模式。
defaultValue defaultValue: Value 同名,类型放宽(任意值类型)。
value value: Value 同上。
onValueChange (value, eventDetails) => void 新增 eventDetailsreason: 'none')。
disabled disabled: boolean,默认 false 相同。
name name: string 相同。
required required: boolean,默认 false 相同。
orientation(默认 undefined 移除 Base UI 的方向键导航自动处理两个轴;无 orientation prop(如辅助技术需要可自行设 aria-orientation)。
dir 移除 DirectionProvider
loop(默认 true 移除 焦点回绕内建且不可配置。

4.2 RadioGroup.Item → Radio.Root;RadioGroup.Indicator → Radio.Indicator

Radix prop Base UI 等价 迁移说明
asChild render 同模式,另有 nativeButton(默认 false)。
valuestring,必填) value: Value,必填 同名,类型放宽。
disabled / required 同名 相同。
forceMount(Indicator) keepMounted: boolean,默认 false 改名。

Base UI 独有:RadioGroup.readOnlyRadioGroup.formRadioGroup.inputRef(group 拥有一个隐藏 input);Radio.Root.readOnlyRadio.Root.inputRefRadio.Root.nativeButton

Data 属性对照:Root data-disabled 保持;Item/Indicator 的 data-state="checked"data-checkeddata-state="unchecked"data-uncheckeddata-disabled 保持;Base UI 新增 data-readonlydata-required、Field 属性,以及 Indicator 的 data-starting-style / data-ending-style。两侧均无 CSS 变量。

五、Switch:签名变化最小的一组

Part 映射:Root -> RootThumb -> Thumb。元素变化:Radix Root 渲染 <button> + 表单内隐藏 input;Base UI Root 始终渲染 <span> 加隐藏 <input>nativeButton + render 渲染真 button)。仓库 switch.tsx 包装组件结构极简:SwitchPrimitive.Root(带 data-slot="switch"data-size)内部仅一个 SwitchPrimitive.Thumb,样式全部走 data-disabled 等 presence 属性选择器——可作为迁移后样式选区改写的直观示例。

5.1 Switch.Root

Radix prop Base UI 等价 迁移说明
asChild render 配合 nativeButton
defaultChecked defaultChecked: boolean,默认 false 相同。
checked checked: boolean 相同。
onCheckedChange (checked, eventDetails) => void 新增 eventDetailsreason: 'none')。
disabled / required / name 同名 相同。
value(Radix 默认 "on" value: string Base UI 默认提交 "on",与原生 checkbox 行为一致。

Switch.ThumbasChildrender,两侧唯一 prop。

Base UI 独有:Root.readOnlyRoot.uncheckedValue(未勾选时提交的值)、Root.formRoot.inputRefRoot.idRoot.nativeButton(默认 false)。

Data 属性对照:Root/Thumb 的 data-state="checked"data-checkeddata-state="unchecked"data-uncheckeddata-disabled 保持;Base UI 新增 data-readonlydata-required 及 Field 属性。两侧均无 CSS 变量。

六、Slider:新增必填 Part Control,结构重排

Part 映射:Root -> RootTrack -> TrackRange -> Indicator(改名)Thumb -> Thumb新增必填 Part Control:Base UI 的解剖结构是 Root > Control > Track > (Indicator, Thumb)Control 是接收点击/拖拽的交互面(Radix 由 Root 自己处理指针交互)。Base UI 另新增 ValueLabel Part。元素变化:Radix Thumb 是外层看不见的 span 包着的普通元素(表单内附隐藏 input);Base UI Thumb 渲染 <div> 且嵌套 <input type="range">

仓库 slider.tsx 是这套新解剖结构的完整落地:Root 内放 Controlcn-slider relative flex ...),Control 内放 TrackTrack 内放 Indicatordata-slot="slider-range",即原 Radix Range 的视觉角色),Thumbvalue/defaultValue 长度循环渲染(见 slider.tsx L20-L48)。注意该包装组件显式设置了 thumbAlignment="edge"——正对应下表 Base UI 默认 'center' 与 Radix 行为差异的补偿写法。

6.1 Slider.Root

Radix prop Base UI 等价 迁移说明
asChild render 同模式。
defaultValuenumber[] defaultValue: number | number[] 放宽:单滑杆可传单个 number,无需数组包裹。
valuenumber[] value: number | number[] 同上;范围滑杆仍传数组。
onValueChange (value, eventDetails) => void value 形状与你传入的一致(单值即 number);eventDetailsreason: 'input-change' | 'track-press' | 'drag' | 'keyboard' | 'none',并带 activeThumbIndex: number
onValueCommit onValueCommitted(改名) 签名同 onValueChange;Base UI 在值未变化时不触发。
name name: string 相同。
disabled disabled: boolean,默认 false 相同。
orientation(默认 "horizontal" orientation: Orientation,默认 'horizontal' 相同。
dir 移除 DirectionProvider
inverted(默认 false 移除(workaround) 无等价物。水平滑杆可包一层 DirectionProvider dir="rtl" 实现方向反转;垂直滑杆无内建反转方式。
min(默认 0)/ max(默认 100)/ step(默认 1 同名同默认 相同。
minStepsBetweenThumbs(默认 0 minStepsBetweenValues: number,默认 0 改名(ThumbsValues)。
form form: string 相同。

6.2 Track、Range/Indicator、Thumb 与新增 Control

Radix Part Base UI Part 关键迁移说明
Track Track(移入 Control) 结构性移动:Track 现在必须嵌在新的 Slider.Control 里,Thumb 移入 Track 内部(Radix 中 Thumb 是 Root 下与 Track 平级的兄弟)。
Range Indicator(改名) 角色相同(可视化已填充部分),仍是 Track 的子元素。
Thumb Thumb asChildrender;Base UI Thumb 新增 index(多滑杆范围滑杆 SSR 时必填)、getAriaLabel(index)getAriaValueText(formattedValue, value, index)aria-valuetext 等可访问性格式化 prop;disabledinputReftabIndexonFocus/onBlur/onKeyDown 转发到嵌套的 <input type="range">
(无) Control(新 Part) Radix 无对应物,即接收指针事件的交互面,用其包裹 Track。Props 仅 className/style/render

Base UI 独有、值得了解的 Slider 能力:

  • Root.thumbAlignment: 'center' | 'edge' | 'edge-client-only'(默认 'center'):min/max 时滑杆中心还是边缘与控件边缘对齐。Radix 的 CSS 表现接近 'edge';要保留滑杆在轨道边界内的观感,显式设 thumbAlignment="edge"(仓库 slider 包装组件即如此处理)。
  • Root.thumbCollisionBehavior: 'push' | 'swap' | 'none'(默认 'push'):范围滑杆滑杆碰撞处理(Radix 行为最接近 'none')——这是必须按 SKILL.md 规则 flag 而非静默修补的行为差异。
  • Root.largeStep: number(默认 10):Page Up/Down 与 Shift+Arrow 的步进。
  • Root.format: Intl.NumberFormatOptionsRoot.locale: Intl.LocalesArgument:供 Slider.Valuearia-valuetext 的值格式化。
  • 新 Part:Slider.Value(渲染 <output>children: (formattedValues: string[], values: number[]) => ReactNode)、Slider.Label(自动关联的 label)。

Data 属性对照:data-disabled(所有 Part)与 data-orientation(所有 Part,取值相同)保持;Base UI 新增 data-dragging(拖拽期间所有 Part 上存在)、Thumb 的 data-index(范围滑杆中滑杆序号)、所有 Part 上的 Field 属性(data-validdata-invaliddata-dirtydata-toucheddata-focused)。两侧均无 CSS 变量(Radix 与 Base UI 都用内联样式定位滑杆)。

七、落地建议:把对照表接到迁移流程上

结合 SKILL.md 的迁移策略,本文档的表格在流程中有明确位置:

  1. 先跑基线npx shadcn@latest info --json 确认当前 base 与 STYLE,检测包管理器,要求干净的 git 树,并在动依赖前先跑一次 typecheck/build 作为基线。
  2. golden pair 优先:shadcn 已知风格(radix-<style>)的项目,直接用 CLI 拉取 base-<style> 变体(整站模式翻 components.jsonshadcn add <component> --overwrite;渐进模式写入 <component>-base.tsx),自定义 wrapper 用 git merge-file user.tsx radix-golden.tsx base-golden.tsx 三方合并后人工消冲突——冲突消解依据正是本文的 Part/Props 表与 class-mapping.md 的 data 属性改写规则。
  3. 每个文件必扫残留grep -n "radix-ui\|@radix-ui\|IconPlaceholder",合并“干净”不等于文件“干净”。
  4. 消费者代码按调用点表扫一遍:表单控件相关的高频断点集中在 consumer-props.md——Checkbox checked="indeterminate" 拆 prop、Slider onValueChange 参数增加且 inverted 移除、Slider onValueCommitonValueCommitted、Select onValueChange 值类型放宽导致 setState 直接传入断型。
  5. 行为差异只 flag 不静默修:例如 Slider 默认 thumbCollisionBehavior='push' 与 Radix 近似 'none'、Select 的 align 默认从 "start"'center'collisionPadding 10→5 与 arrowPadding 0→5 等默认值漂移,都应写入 .migration/<component>.md 的 “Behavior changes” 小节(结构见 SKILL.md),由使用者决策。
  6. 人工验证清单对应原文档各组件的交互特性:Select 的键盘导航 + typeahead(Base UI 中 label prop 承接原 textValue)、Checkbox 半选三态切换、Slider 的 commit 事件时机(值未变化不触发 onValueCommitted)。

八、快速对照速查

主题 规则
asChild → 所有 Part 的 renderclassName/style 可传状态回调
渲染目标为原生 button nativeButton(默认值因 Part 而异,Trigger 为 true,Item/Root 类多为 false
拦截默认关闭 onOpenChangeeventDetails.reason + cancel(),替代 onEscapeKeyDown/onPointerDownOutside/onCloseAutoFocus
data-state → presence 属性(data-checked/data-selected/data-open 等);类名选择器按各组件 data 表重写
Radix dir / loop / inverted / orientation(radio) 无对应,用 DirectionProvider 或删除(垂直滑杆反转无内建方案)
Select Content 定位 属性整体搬到 Positionerposition 枚举 → alignItemWithTrigger 布尔;注意 4 处默认值漂移(aligncollisionBoundarycollisionPaddingarrowPadding
Checkbox 三态 checked="indeterminate" → 独立 indeterminate prop;可配 parent + CheckboxGroup 做全选
RadioGroup 命名空间 RadioGroup.Item/IndicatorRadio.Root/Indicator
Slider 结构 新增必填 ControlRangeIndicatoronValueCommitonValueCommittedinverted 移除;thumbAlignment 建议显式 "edge"
CSS 变量 仅 Select 有:--radix-select-*--anchor-*/--available-*/--transform-origin,宿主从 Content 移到 Positioner

以上所有映射以 form-controls.md 原文为权威来源;遇到表外 prop 或 Part,按 SKILL.md 的要求先查 node_modules/@base-ui/react/**/*.d.ts 再动手,并在迁移报告中记录缺口,而不是猜测。

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