shadcn/ui 迁移指南:Radix UI 到 Base UI 表单控件(Select、Checkbox、Radio、Switch、Slider)的 Props 对照与实战
本篇以 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.tsx、slider.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”几乎总是编译不过。
asChild→render。Radix 的asChild(boolean,默认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/>}>...。nativeButton是 Base UI 独有。渲染交互式元素的 Base UI Part 接受nativeButton,用于告诉 Base UI 你的render目标是不是原生<button>——Radix 没有对应物。默认值因 Part 而异(Select.Trigger 默认为true,Select.Item、Checkbox.Root、Switch.Root 等默认为false),渲染非 button 节点时要显式设置。- 回调追加
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 必须逐条核对。 data-state="x"拆成 presence 属性。Radix 的单一data-statetoken 在 Base UI 里变成data-checked、data-unchecked、data-open等存在性属性;Base UI 还普遍附加 Field 集成属性(data-valid、data-invalid、data-dirty、data-touched、data-filled、data-focused)和动画钩子(data-starting-style、data-ending-style)。同时 Radix 的dirprop 没有逐组件等价物,方向统一由DirectionProvider(或祖先元素的dirHTML 属性)决定——注意 SKILL.md 提醒的是directionprop 而非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 -> Root、Trigger -> Trigger、Value -> Value、Icon -> Icon、Portal -> Portal、Content -> Portal > Positioner > Popup(一个 Part 拆成三层)、Viewport -> List、Item -> Item、ItemText -> ItemText、ItemIndicator -> ItemIndicator、ScrollUpButton -> ScrollUpArrow、ScrollDownButton -> ScrollDownArrow、Group -> Group、Label -> GroupLabel、Separator -> Separator、Arrow -> 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 上承接 side、sideOffset、align、alignOffset、alignItemWithTrigger 五个定位属性,Popup 内按 ScrollUpButton > List > ScrollDownButton 顺序嵌套子项;SelectLabel 包装组件则直接使用 SelectPrimitive.GroupLabel(见 select.tsx L66-L106 与 L108-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 |
签名变化:第二参数 eventDetails 带 reason('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 侧:asChild → render;placeholder 同名保留,但行为有变化: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 |
移过去,类型放宽。 |
avoidCollisions(boolean,默认 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,改为传 finalFocus:boolean | 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(改名) |
两侧除 asChild→render 外无其他 prop。结构变化:Radix 要求 ScrollUpButton/Viewport/ScrollDownButton 作为 Content 的兄弟;Base UI 中 List 与滚动箭头是 Popup 的子元素。 |
Item |
Item |
asChild→render,另有 nativeButton(默认 false,Base UI Item 渲染 <div>)。value 由必填 string 放宽为 any(允许对象,配合 Root 的 isItemEqualToValue/itemToString*);null value 标记 placeholder 项。textValue → label(改名,同样驱动 typeahead 文本匹配,默认取 item 文本内容)。 |
ItemText |
ItemText |
asChild→render;注意元素变化: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 |
仅 asChild→render。 |
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.items:Record<string, ReactNode> | { label, value }[] | Group[],让Select.Value渲染标签而非原始值。Root.isItemEqualToValue、Root.itemToStringLabel、Root.itemToStringValue:对象值支持。Root.modal: boolean(默认true):滚动锁定 + 阻挡外部指针;Radix select 一直是模态式的,要非模态行为需显式modal={false}。Root.readOnly、Root.autoComplete、Root.form、Root.inputRef、Root.id、Root.onOpenChangeComplete、Root.actionsRef(提供{ unmount() }用于外部控制的退出动画)、Root.highlightItemOnHover(默认true)。- 新 Part:
Select.Backdrop(弹窗下的遮罩层)、Select.Label(trigger 标签)、Popup.finalFocus。 Positioner.anchor、Positioner.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-pressed、data-popup-side |
Trigger data-placeholder |
Trigger/Value data-placeholder(相同) |
Trigger data-disabled |
Trigger data-disabled(相同),另有 data-readonly、data-required 及 Field 属性 |
Content data-state="open" | "closed" |
Positioner/Popup data-open / data-closed(presence) |
Content data-side(left/right/bottom/top) |
Positioner/Popup data-side(none/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 -> Root、Indicator -> 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。 |
defaultChecked(boolean | 'indeterminate') |
defaultChecked: boolean,默认 false |
签名变化:'indeterminate' 不再是 checked 的取值,改用独立的 indeterminate: boolean prop。 |
checked(boolean | 'indeterminate') |
checked: boolean + indeterminate: boolean |
拆成两个 prop:Radix checked="indeterminate" → Base UI indeterminate(半选与勾选/未勾选相互独立)。 |
onCheckedChange((checked: boolean | 'indeterminate') => void) |
(checked: boolean, eventDetails) => void |
checked 恒为布尔,新增 eventDetails(reason: '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 独有能力
asChild→render;forceMount→keepMounted: boolean(默认false)。两者都用于未勾选时保留元素在 DOM(配合动画);Base UI 在indeterminate时也会渲染 indicator。- Base UI 独有:
Root.indeterminate(与checked解耦的混合态);Root.parent: boolean+ 全新组件CheckboxGroup(value/defaultValue/onValueChange(string[], eventDetails)/allValues/disabled)——为一组 checkbox 提供共享状态,支持父级“全选”checkbox,Radix 无等价物;以及Root.readOnly、Root.uncheckedValue(未勾选时提交的值)、Root.form、Root.inputRef、Root.id、Root.nativeButton。
Data 属性对照:data-state="checked" → data-checked;data-state="unchecked" → data-unchecked;data-state="indeterminate" → data-indeterminate;data-disabled 保持不变;Base UI 新增 data-readonly、data-required 及 Field 属性(data-valid、data-invalid、data-dirty、data-touched、data-filled、data-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)和 Radio(Radio.Root、Radio.Indicator)。具体为:RadioGroup.Root -> RadioGroup、RadioGroup.Item -> Radio.Root、RadioGroup.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 |
新增 eventDetails(reason: '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)。 |
value(string,必填) |
value: Value,必填 |
同名,类型放宽。 |
disabled / required |
同名 | 相同。 |
forceMount(Indicator) |
keepMounted: boolean,默认 false |
改名。 |
Base UI 独有:RadioGroup.readOnly、RadioGroup.form、RadioGroup.inputRef(group 拥有一个隐藏 input);Radio.Root.readOnly、Radio.Root.inputRef、Radio.Root.nativeButton。
Data 属性对照:Root data-disabled 保持;Item/Indicator 的 data-state="checked" → data-checked、data-state="unchecked" → data-unchecked;data-disabled 保持;Base UI 新增 data-readonly、data-required、Field 属性,以及 Indicator 的 data-starting-style / data-ending-style。两侧均无 CSS 变量。
五、Switch:签名变化最小的一组
Part 映射:Root -> Root、Thumb -> 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 |
新增 eventDetails(reason: 'none')。 |
disabled / required / name |
同名 | 相同。 |
value(Radix 默认 "on") |
value: string |
Base UI 默认提交 "on",与原生 checkbox 行为一致。 |
Switch.Thumb:asChild → render,两侧唯一 prop。
Base UI 独有:Root.readOnly、Root.uncheckedValue(未勾选时提交的值)、Root.form、Root.inputRef、Root.id、Root.nativeButton(默认 false)。
Data 属性对照:Root/Thumb 的 data-state="checked" → data-checked、data-state="unchecked" → data-unchecked;data-disabled 保持;Base UI 新增 data-readonly、data-required 及 Field 属性。两侧均无 CSS 变量。
六、Slider:新增必填 Part Control,结构重排
Part 映射:Root -> Root、Track -> Track、Range -> Indicator(改名)、Thumb -> Thumb,新增必填 Part Control:Base UI 的解剖结构是 Root > Control > Track > (Indicator, Thumb)。Control 是接收点击/拖拽的交互面(Radix 由 Root 自己处理指针交互)。Base UI 另新增 Value 和 Label Part。元素变化:Radix Thumb 是外层看不见的 span 包着的普通元素(表单内附隐藏 input);Base UI Thumb 渲染 <div> 且嵌套 <input type="range">。
仓库 slider.tsx 是这套新解剖结构的完整落地:Root 内放 Control(cn-slider relative flex ...),Control 内放 Track,Track 内放 Indicator(data-slot="slider-range",即原 Radix Range 的视觉角色),Thumb 按 value/defaultValue 长度循环渲染(见 slider.tsx L20-L48)。注意该包装组件显式设置了 thumbAlignment="edge"——正对应下表 Base UI 默认 'center' 与 Radix 行为差异的补偿写法。
6.1 Slider.Root
| Radix prop | Base UI 等价 | 迁移说明 |
|---|---|---|
asChild |
render |
同模式。 |
defaultValue(number[]) |
defaultValue: number | number[] |
放宽:单滑杆可传单个 number,无需数组包裹。 |
value(number[]) |
value: number | number[] |
同上;范围滑杆仍传数组。 |
onValueChange |
(value, eventDetails) => void |
value 形状与你传入的一致(单值即 number);eventDetails 的 reason: '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 |
改名(Thumbs → Values)。 |
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 |
asChild→render;Base UI Thumb 新增 index(多滑杆范围滑杆 SSR 时必填)、getAriaLabel(index)、getAriaValueText(formattedValue, value, index)、aria-valuetext 等可访问性格式化 prop;disabled、inputRef、tabIndex、onFocus/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.NumberFormatOptions与Root.locale: Intl.LocalesArgument:供Slider.Value与aria-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-valid、data-invalid、data-dirty、data-touched、data-focused)。两侧均无 CSS 变量(Radix 与 Base UI 都用内联样式定位滑杆)。
七、落地建议:把对照表接到迁移流程上
结合 SKILL.md 的迁移策略,本文档的表格在流程中有明确位置:
- 先跑基线:
npx shadcn@latest info --json确认当前 base 与 STYLE,检测包管理器,要求干净的 git 树,并在动依赖前先跑一次 typecheck/build 作为基线。 - golden pair 优先:shadcn 已知风格(
radix-<style>)的项目,直接用 CLI 拉取base-<style>变体(整站模式翻components.json后shadcn add <component> --overwrite;渐进模式写入<component>-base.tsx),自定义 wrapper 用git merge-file user.tsx radix-golden.tsx base-golden.tsx三方合并后人工消冲突——冲突消解依据正是本文的 Part/Props 表与 class-mapping.md 的 data 属性改写规则。 - 每个文件必扫残留:
grep -n "radix-ui\|@radix-ui\|IconPlaceholder",合并“干净”不等于文件“干净”。 - 消费者代码按调用点表扫一遍:表单控件相关的高频断点集中在 consumer-props.md——Checkbox
checked="indeterminate"拆 prop、SlideronValueChange参数增加且inverted移除、SlideronValueCommit→onValueCommitted、SelectonValueChange值类型放宽导致setState直接传入断型。 - 行为差异只 flag 不静默修:例如 Slider 默认
thumbCollisionBehavior='push'与 Radix 近似'none'、Select 的align默认从"start"变'center'、collisionPadding10→5 与arrowPadding0→5 等默认值漂移,都应写入.migration/<component>.md的 “Behavior changes” 小节(结构见 SKILL.md),由使用者决策。 - 人工验证清单对应原文档各组件的交互特性:Select 的键盘导航 + typeahead(Base UI 中
labelprop 承接原textValue)、Checkbox 半选三态切换、Slider 的 commit 事件时机(值未变化不触发onValueCommitted)。
八、快速对照速查
| 主题 | 规则 |
|---|---|
asChild |
→ 所有 Part 的 render;className/style 可传状态回调 |
| 渲染目标为原生 button | 设 nativeButton(默认值因 Part 而异,Trigger 为 true,Item/Root 类多为 false) |
| 拦截默认关闭 | onOpenChange 的 eventDetails.reason + cancel(),替代 onEscapeKeyDown/onPointerDownOutside/onCloseAutoFocus |
data-state |
→ presence 属性(data-checked/data-selected/data-open 等);类名选择器按各组件 data 表重写 |
Radix dir / loop / inverted / orientation(radio) |
无对应,用 DirectionProvider 或删除(垂直滑杆反转无内建方案) |
| Select Content 定位 | 属性整体搬到 Positioner;position 枚举 → alignItemWithTrigger 布尔;注意 4 处默认值漂移(align、collisionBoundary、collisionPadding、arrowPadding) |
| Checkbox 三态 | checked="indeterminate" → 独立 indeterminate prop;可配 parent + CheckboxGroup 做全选 |
| RadioGroup 命名空间 | RadioGroup.Item/Indicator → Radio.Root/Indicator |
| Slider 结构 | 新增必填 Control;Range → Indicator;onValueCommit → onValueCommitted;inverted 移除;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 再动手,并在迁移报告中记录缺口,而不是猜测。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00