首页
/ shadcn ui 菜单组件迁移手册:Radix UI 到 Base UI 的 dropdown-menu、context-menu、menubar 与 navigation-menu 完整映射

shadcn ui 菜单组件迁移手册:Radix UI 到 Base UI 的 dropdown-menu、context-menu、menubar 与 navigation-menu 完整映射

2026-09-06 09:50:25作者:郦嵘贵Just

本文基于仓库中的迁移知识库 menus.md(配套技能入口见 SKILL.md),系统讲解 shadcn 项目中菜单组件家族(dropdown-menu / context-menu / menubar / navigation-menu)从 Radix UI 迁移到 @base-ui/react(仓库当前锁定 1.6.0,见 apps/v4/package.json)时每个 part 的 props 改名、签名变化、默认值翻转与行为差异。读完后,你可以独立完成任意菜单组件的迁移,并准确处理 asChild → renderContent → Portal > Positioner > Popup 结构性重组、事件回调统一为 onOpenChange(open, eventDetails)、data 属性与 CSS 变量改写这四类核心问题。

一、迁移事实来源与 part 级总映射

这份映射表不是推测:其“ground truth”来自本仓库注册表中 61 组 Radix / Base 双风格组件对的机械 diff(universal-patterns.md 说明数据来自 apps/v4/registry/bases/{radix,base}/ui/ 的成对文件),Radix 侧类型/默认值提取自 radix-ui 官方页面内嵌类型载荷,Base UI 侧对应 1.6.0 文档(Base UI 1.x)。仓库中同时保留了两侧 wrapper 源码可供对照,例如 Radix 版 dropdown-menu.tsx 与 Base 版 dropdown-menu.tsx

先看菜单家族中最重要的 part 级映射(从 wrapper 对照得出的 ground truth):

Radix part Base UI part
Content Portal > Positioner > Popupside / sideOffset / align / alignOffset 全部落到 Positioner 上)
Label GroupLabel
ItemIndicator CheckboxItemIndicator / RadioItemIndicator
Sub SubmenuRoot
SubTrigger SubmenuTrigger
navigation-menu Viewport Positioner > Popup > Viewport
navigation-menu Indicator Icon
asChild render

这一结构变化在 Base 版 dropdown-menu wrapper 中有直接体现:DropdownMenuContent 内部就是 MenuPrimitive.PortalMenuPrimitive.Positioner 再包 MenuPrimitive.Popup,且定位 props 通过 Pick<MenuPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset"> 声明后逐个显式传给 Positioner(apps/v4/registry/bases/base/ui/dropdown-menu.tsx)。universal-patterns.md 特别警告了这里的“转发规则(FORWARD rule)”:如果你用 Pick 声明了定位 props,却没有在 wrapper 里解构并显式传给 Positioner,它们会经 ...props 落到 Popup 这个错误的 DOM 节点上,定位会静默失效,且没有任何 JSX 层面的类型错误能捕获。迁移自写 wrapper 时务必遵守“声明 → 解构 → 转发”三步。

二、跨组件通用规则(适用于所有 part)

以下规则对下文每个 part 表格中的同名 props 一律适用,是迁移时最高频的改动点:

Radix 模式 Base UI 对应
asChildboolean,默认 false renderReactElement | ((props: HTMLProps, state) => ReactElement))。没有“合并到子元素”的布尔开关,直接传元素或函数。
Root 上的 dir"ltr" | "rtl" 全部移除。Base UI 从 <DirectionProvider>@base-ui-components/react/direction-provider)或 DOM dir 属性读取方向。
forceMountboolean Portal / indicator 部件上的 keepMountedboolean,默认 false)。使用场景相同(动画/SEO),但显隐存在改为 CSS 驱动:用 data-starting-style / data-ending-style 替代 Radix 的 data-state + 强制挂载模式。
内容部件上的 onEscapeKeyDown / onPointerDownOutside / onFocusOutside / onInteractOutside 不再作为独立 props 存在。统一改用 Root 上的 onOpenChange(open, eventDetails),按 eventDetails.reason 分支('escape-key''outside-press''focus-out' 等);调用 eventDetails.cancel() 可阻止关闭(替代原来的 event.preventDefault())。
Item 上的 onSelect(event: Event) => voidevent.preventDefault() 可保持菜单打开) onClick(event: BaseUIEvent<React.MouseEvent<HTMLDivElement>>) => void)加 closeOnClickboolean)控制点击后是否关闭菜单。
Item 上的 textValuestring,typeahead 文本) labelstring)。
受控回调 (value) => void 所有 Base UI 变更回调都带第二个 eventDetails 参数({ reason, event, cancel(), allowPropagation(), isCanceled, isPropagationAllowed, trigger })。

对应地,动画惯用法也要整体改写(详见 class-mapping.md):Radix 的 data-[state=open]:animate-in data-[state=closed]:animate-out(keyframes)应重写为 transition-[opacity,transform] data-starting-style:opacity-0 data-ending-style:opacity-0(transition + 起止样式),不要把 animate-in/out 工具类 1:1 照搬。

三、dropdown-menu:Radix DropdownMenu → Base UI Menu

Root → Menu.Root

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
defaultOpen boolean / – defaultOpenboolean,默认 false 不变。
open boolean / – openboolean 不变。
onOpenChange (open: boolean) => void / – onOpenChange 签名变化:(open: boolean, eventDetails: Menu.Root.ChangeEventDetails) => voideventDetails.reason 取值于 'trigger-hover' | 'trigger-focus' | 'trigger-press' | 'outside-press' | 'focus-out' | 'list-navigation' | 'escape-key' | 'item-press' | 'close-press' | 'sibling-open' | 'cancel-open' | 'imperative-action' | 'none'eventDetails.cancel() 可阻断状态变化。
modal boolean / true modalboolean,默认 true 不变。
dir "ltr" | "rtl" / – 移除,改用 DirectionProvider

Trigger → Menu.Trigger

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
asChild boolean / false render 见通用规则。渲染非 button 元素时还要设置 nativeButton={false}

Portal → Menu.Portal

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
forceMount boolean / – keepMountedboolean,默认 false 改名;隐藏时保留 portal 在 DOM 中。
container HTMLElement / document.body containerHTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null 类型放宽(接受 ref 和 ShadowRoot)。

Content → Menu.Portal > Menu.Positioner > Menu.Popup

这是 props 迁移量最大的 part,逐条列出:

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
asChild boolean / false Popup 上的 render 见通用规则。
loop boolean / false Root 上的 loopFocusboolean,默认 true 移动 + 改名,且默认值翻转:Base UI 默认循环焦点。
onCloseAutoFocus (event: Event) => void / – Popup 上的 finalFocus 签名变化:boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null),其中 InteractionType = 'mouse' | 'touch' | 'pen' | 'keyboard'。返回 false 等价于原来的 event.preventDefault();返回元素则重定向焦点。
onEscapeKeyDown (event: KeyboardEvent) => void / – 移除 → onOpenChangereason === 'escape-key'
onPointerDownOutside (event: PointerDownOutsideEvent) => void / – 移除 → reason === 'outside-press'
onFocusOutside (event: FocusOutsideEvent) => void / – 移除 → reason === 'focus-out'
onInteractOutside (event: PointerDownOutsideEvent | FocusOutsideEvent) => void / – 移除 → reason === 'outside-press' || 'focus-out'
forceMount boolean / – Portal 上的 keepMounted 移动;动画改用 data-starting-style/data-ending-style
side "top" | "right" | "bottom" | "left" / "bottom" Positioner 上的 sideSide,默认 'bottom' 移动。Base 增加逻辑值:Side = 'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start'
sideOffset number / 0 Positioner 上的 sideOffsetnumber | OffsetFunction,默认 0 移动;还接受函数形式 (data: { side, align, anchor: {width,height}, positioner: {width,height} }) => number
align "start" | "center" | "end" / "center" Positioner 上的 alignAlign,默认 'center' 移动,取值与默认值相同。
alignOffset number / 0 Positioner 上的 alignOffsetnumber | OffsetFunction,默认 0 移动;同样接受函数形式。
avoidCollisions boolean / true Positioner 上的 collisionAvoidanceCollisionAvoidance 签名变化:对象 { side?: 'flip' | 'shift' | 'none'; align?: 'flip' | 'shift' | 'none'; fallbackAxisSide?: 'start' | 'end' | 'none' }avoidCollisions={false} 对应 collisionAvoidance={{ side: 'none', align: 'none', fallbackAxisSide: 'none' }}
collisionBoundary Element | null | Array<Element | null> / [] Positioner 上的 collisionBoundaryBoundary,默认 'clipping-ancestors' 默认值变化:Radix [] 表示视口/裁剪祖先;Base 默认 'clipping-ancestors' 与之等价。也接受元素或 rect。
collisionPadding number | Padding / 0 Positioner 上的 collisionPaddingPadding,默认 5 形状相同;默认值从 0 变为 5。
arrowPadding number / 0 Positioner 上的 arrowPaddingnumber,默认 5 相同;默认值从 0 变为 5。
sticky "partial" | "always" / "partial" –(见说明) 概念不同。Radix sticky 控制 align 轴吸附,最接近的 Base 旋钮是 collisionAvoidance.align'shift' ≈ partial)。Base 自己的 stickyboolean,默认 false)是让 popup 在锚点滚出视口后仍留在视口内,Radix 没有对应物。
hideWhenDetached boolean / false 作为行为 prop 被移除。Base 始终在 Positioner/Popup 上暴露 data-anchor-hidden;用 CSS 隐藏:[data-anchor-hidden] { visibility: hidden }

Arrow → Menu.Arrow

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
asChild boolean / false render Base Arrow 渲染一个由你填入 SVG 的 <div>(Radix 直接渲染 svg)。放在 Popup 内部。
width number / 10 移除;用 CSS 设置子 SVG/元素尺寸。
height number / 5 移除;用 CSS 设置。

Item → Menu.Item

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
asChild boolean / false render 链接场景用 Menu.LinkItem(渲染 <a>)而不是 render
disabled boolean / – disabledboolean,默认 false 不变。
onSelect (event: Event) => void / – onClick(event: BaseUIEvent<React.MouseEvent<HTMLDivElement>>) => void 改名 + 签名变化。原来在 onSelectevent.preventDefault() 保持菜单打开 → 改为 closeOnClick={false}boolean,Item 上默认 true)。
textValue string / – labelstring 改名。

Group / Label

part Radix prop 类型 / 默认值 Base UI 对应 迁移说明
GroupMenu.Group asChild boolean / false render 其余不变。
LabelMenu.GroupLabel asChild boolean / false render part 改名。Base 的 GroupLabel 必须位于 Group 内部(它负责接线 aria-labelledby);Radix Label 可以游离使用。

CheckboxItem → Menu.CheckboxItem

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
asChild boolean / false render
checked boolean | 'indeterminate' / – checkedboolean 'indeterminate' 被移除。Base 新增非受控用 defaultCheckedboolean,默认 false)。
onCheckedChange (checked: boolean) => void / – onCheckedChange 签名变化:(checked: boolean, eventDetails: Menu.CheckboxItem.ChangeEventDetails) => void
disabled boolean / – disabledboolean,默认 false 不变。
onSelect (event: Event) => void / – onClick + closeOnClick 行为默认翻转:Radix 选中即关闭(除非 preventDefault);Base 的 CheckboxItem 上 closeOnClick 默认 false。要保留 Radix 行为需显式设置 closeOnClick
textValue string / – label 改名。

RadioGroup / RadioItem / ItemIndicator

part Radix prop 类型 / 默认值 Base UI 对应 迁移说明
RadioGroupMenu.RadioGroup asChild boolean / false render
value string / – valueany 类型放宽为 any。Base 新增 defaultValueany)与 disabledboolean,默认 false)。
onValueChange (value: string) => void / – onValueChange 签名变化:(value: any, eventDetails: Menu.RadioGroup.ChangeEventDetails) => void
RadioItemMenu.RadioItem asChild boolean / false render
value* string / – value*(any 不变(必填);类型放宽。
disabled boolean / – disabledboolean,默认 false 不变。
onSelect (event: Event) => void / – onClick + closeOnClick RadioItem 上 closeOnClick 默认 false(Radix 默认关闭)。
textValue string / – label 改名。
ItemIndicatorMenu.CheckboxItemIndicator / Menu.RadioItemIndicator asChild boolean / false render part 拆分:按父 item 类型选用对应 indicator,渲染 <span>
forceMount boolean / – keepMountedboolean,默认 false 改名。

Separator / Sub / SubTrigger / SubContent

part Radix prop 类型 / 默认值 Base UI 对应 迁移说明
SeparatorMenu.Separator asChild boolean / false render Base 新增 orientation'horizontal' | 'vertical',默认 'horizontal')。
SubMenu.SubmenuRoot defaultOpen boolean / – defaultOpenboolean,默认 false 不变。
open boolean / – openboolean 不变。
onOpenChange (open: boolean) => void / – onOpenChange 签名变化:(open: boolean, eventDetails: Menu.SubmenuRoot.ChangeEventDetails) => void(与 Root 相同的 reason 联合类型)。
SubTriggerMenu.SubmenuTrigger asChild boolean / false render 渲染 <div>;此处 nativeButton 默认 false
disabled boolean / – disabledboolean,默认 false 不变。
textValue string / – label 改名。Base 另增 openOnHover / delay100)/ closeDelay0)与 onClick

SubContentMenu.Portal > Menu.Positioner > Menu.Popup(位于 SubmenuRoot 内部):props 归宿与上面 Content 完全相同,但需注意两条 Radix 特有默认值——Radix SubContent 的 align 默认是 "start",而 Base Positioner 默认 'center';如果依赖了 Radix 默认值要显式写 align="start"(实践中子菜单 popup 锚定在触发项上,本仓库 wrapper 已在 SubContent 中把 align="start"alignOffset=-3side="right" 作为默认值处理)。另外 Radix SubContent 没有 side prop(方向隐含),Base Positioner 接受 side(RTL 感知的子菜单建议用 'inline-end')。所有 outside/escape 回调、forceMountloop、collision 系列 props 的映射与 Content 一致。

Base UI 独有的值得了解的 props(Menu)

  • RoothighlightItemOnHover(默认 true)、actionsRef{ unmount(), close() })、onOpenChangeComplete(open)(关闭动画结束后触发,替代 Radix“等动画结束”的绕行写法)、closeParentOnEsc(默认 false)、disabledorientation(默认 'vertical')、detached-trigger 机制:handleMenu.Handle,经 Menu.createHandle() 创建)、triggerId/defaultTriggerId、感知 payload 的 children 渲染函数。
  • TriggeropenOnHoverdelay100)、closeDelay0)、payloadhandlenativeButton(默认 true)。
  • Backdrop:新增 part,popup 下方的遮罩层。
  • PositioneranchorpositionMethod'absolute')、disableAnchorTrackingsticky(boolean,视口保持)。
  • PopupfinalFocus
  • Viewport:新增 part,用于多触发器/detached 触发器之间内容切换的动画。
  • LinkItem:新增 part,渲染 <a> 的菜单项(closeOnClick 默认 false)。
  • 所有 part:className/style 接受状态回调形式 (state) => ...

四、data 属性映射(dropdown / context / menubar 菜单通用)

样式代码里大量 data-[state=...] 选择器需要按 class-mapping.md 做机械改写,菜单家族对应关系如下:

Radix Base UI
Trigger [data-state="open" | "closed"] data-popup-open(存在性属性)+ data-pressed
Content [data-state="open" | "closed"] Positioner 和 Popup 上的 data-open / data-closed
data-starting-style / data-ending-style(CSS transition 钩子,替代对 data-state 做动画)
Content [data-side="left" | "right" | "bottom" | "top"] Positioner/Popup/Arrow 上的 data-side'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start'
Content [data-align="start" | "end" | "center"] Positioner/Popup/Arrow 上的 data-align(取值相同)
Content/Item [data-orientation] 菜单 part 上移除
Item [data-highlighted] data-highlighted(不变)
Item [data-disabled] data-disabled(不变)
Checkbox/RadioItem [data-state="checked" | "unchecked" | "indeterminate"] data-checked / data-unchecked 存在性属性;无 indeterminate
ItemIndicator [data-state] 拆分后的 indicator 上 data-checked / data-unchecked + data-starting-style / data-ending-style
SubTrigger [data-state="open" | "closed"] SubmenuTrigger 上的 data-popup-open
Popup 的 data-instant'click' | 'dismiss' | 'group' | 'trigger-change')、Positioner 的 data-anchor-hidden

仓库 Base 版 wrapper 已经体现了这些新钩子,例如 SubTrigger 的高亮类从 data-[state=open]: 改为 data-popup-open:bg-accent data-popup-open:text-accent-foregroundapps/v4/registry/bases/base/ui/dropdown-menu.tsx),Popup 的溢出控制类从 data-[state=closed]:overflow-hidden 改为 data-closed:overflow-hidden(同文件 L45)。

五、CSS 变量映射(dropdown-menu / context-menu / menubar)

Radix(在 Content/SubContent 上) Base UI(在 Positioner 上)
--radix-<name>-content-transform-origin --transform-origin
--radix-<name>-content-available-width --available-width
--radix-<name>-content-available-height --available-height
--radix-<name>-trigger-width --anchor-width
--radix-<name>-trigger-height --anchor-height

Base UI Menu 的 Viewport 另暴露 --popup-width / --popup-height(过渡期间的前一内容尺寸)。两侧 wrapper 的直接对比就是这条映射的活证据:Radix 版 Content 使用 w-(--radix-dropdown-menu-trigger-width)max-h-(--radix-dropdown-menu-content-available-height)origin-(--radix-dropdown-menu-content-transform-origin)radix/ui/dropdown-menu.tsx L47),Base 版对应改为 w-(--anchor-width)max-h-(--available-height)origin-(--transform-origin)base/ui/dropdown-menu.tsx L45)。

六、context-menu:Radix ContextMenu → Base UI ContextMenu

Base UI ContextMenu 与 Menu 共享同一套 part:Root, Trigger, Portal, Backdrop, Positioner, Popup, Arrow, Item, Group, GroupLabel, Separator, SubmenuRoot, SubmenuTrigger, RadioGroup, RadioItem, RadioItemIndicator, CheckboxItem, CheckboxItemIndicator, LinkItem。下文未单独列出的部分,映射与 dropdown-menu 章节完全一致(独立子路径为 @base-ui/react/context-menu,见 base/ui/context-menu.tsx)。

Root → ContextMenu.Root

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
dir "ltr" | "rtl" / – 移除;用 DirectionProvider
open boolean / – openboolean 不变。Base 另加 defaultOpen(默认 false),这是 Radix ContextMenu 原本没有的。
onOpenChange (open: boolean) => void / – onOpenChange 签名变化:(open: boolean, eventDetails: ContextMenu.Root.ChangeEventDetails) => void(与 Menu 相同的 reason 联合)。
modal boolean / true 移除。 Base UI ContextMenu.Root 没有 modal prop(行为固定)。若你依赖 modal={false},没有直接等价物。

Trigger → ContextMenu.Trigger

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
asChild boolean / false render Base Trigger 渲染 <div>(同时处理触摸长按)。
disabled boolean / false 移除。 ContextMenu.Trigger 只有 className/style/render。变通:在 Trigger 外条件性渲染内容,或在子元素上拦截 onContextMenupreventDefault + stopPropagation

Portal / Content

Portal 与 Menu.Portal 完全相同(forceMountkeepMountedcontainer 放宽)。

Content → ContextMenu.Portal > Positioner > Popup,与 dropdown-menu Content 相同的归宿:asChildloop(→ Root loopFocus)、onCloseAutoFocus(→ Popup finalFocus)、onEscapeKeyDown/onPointerDownOutside/onFocusOutside/onInteractOutside(→ Root onOpenChange reason)、forceMount(→ Portal keepMounted)、avoidCollisions(→ collisionAvoidance)、collisionBoundary(默认 []'clipping-ancestors')、collisionPadding05)、sticky(移除,见 Menu 说明)、hideWhenDetached(→ data-anchor-hidden 上的 CSS)。

Radix 特有差异:

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
alignOffset number / 0 Positioner 上的 alignOffsetnumber | OffsetFunction,默认 0 Radix ContextMenu.Content 没有 side/sideOffset/align props(锚定在指针上)。Base Positioner 仍接受 side/align/sideOffset,但默认锚定指针位置;通常保持不设置。
(Content 上无 arrowPadding Positioner 上的 arrowPadding5 若加 Arrow,Base 提供该 prop。

Arrow / Item / Group / Label / CheckboxItem / RadioGroup / RadioItem / ItemIndicator / Separator / Sub / SubTrigger / SubContent 的归宿与 dropdown-menu 章节完全一致(Label → GroupLabel,ItemIndicator → CheckboxItemIndicator/RadioItemIndicator,Sub → SubmenuRoot,SubTrigger → SubmenuTrigger,SubContent → Portal > Positioner > Popup)。Base 中 Menubar/ContextMenu 的子菜单就是相同 props 的同一组组件(onOpenChange eventDetails、labelonClick/closeOnClickkeepMounted)。

Base UI 独有能力、data 属性、CSS 变量与 Menu 章节相同;ContextMenu.Root 额外支持 handleMenuHandle<unknown>)、triggerId/defaultTriggerIdactionsRefonOpenChangeCompletehighlightItemOnHovercloseParentOnEscdisabledorientation。Trigger data 属性:data-popup-opendata-pressed(替代 Radix Trigger 的 [data-state])。CSS 变量:--radix-context-menu-* → Positioner 上的 --transform-origin / --available-* / --anchor-*

七、menubar:Radix Menubar → Base UI Menubar + Menu

Base UI 的 menubar 模块只导出一个 <Menubar> 容器,其中每个菜单都由 Menu.* part 构建(Menu.RootMenu.TriggerMenu.PortalMenu.PositionerMenu.Popup、items、submenus…)。因此 Radix Menubar.Menu/Trigger/Portal/Content/... 的 part 全部落到 dropdown-menu 章节的 Menu 组件族上。仓库 Base 版 menubar.tsx 正是这种“委托”结构的直接实现:MenubarMenu 直接复用 DropdownMenuMenubarGroup/MenubarPortal 等也全部包装 DropdownMenu 族组件(apps/v4/registry/bases/base/ui/menubar.tsx)。

Root → Menubar

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
asChild boolean / false render 同模式。
defaultValue string / – 移除。 Base Menubar 没有受控/非受控的“当前活动菜单”值。要预打开某个菜单,用该 Menu.RootdefaultOpen
value string / – 移除。 改为控制各个 Menu.Rootopen prop。
onValueChange (value: string) => void / – 移除。 通过每个 Menu.RootonOpenChange 监听。
dir "ltr" | "rtl" / – 移除;用 DirectionProvider
loop boolean / false loopFocusboolean,默认 true 改名;默认值翻转为 true

Base UI Menubar 独有:modalboolean,默认 true)、disabledboolean,默认 false)、orientation'horizontal' \| 'vertical',默认 'horizontal')。

Menu / Trigger

part Radix prop 类型 / 默认值 Base UI 对应 迁移说明
MenuMenu.Root asChild boolean / false Menu.Root 不渲染元素,直接删掉。
value string / – 随 Menubar value 体系一起移除(见 Root)。
TriggerMenu.Trigger asChild boolean / false render(非 button 时加 nativeButton={false} Radix Menubar.Trigger 的 [data-state]/[data-highlighted]/[data-disabled]data-popup-open/data-pressed(Base trigger 没有 highlighted 状态)。

注意:Menubar 内部的 Menu.Root 接受全部 Menu.Root props(opendefaultOpenonOpenChange(open, eventDetails)modalloopFocusorientationdisabled…),并且 menubar 各菜单之间的悬停切换是内置的。

Portal / Content / Arrow / Item / Group / Label / CheckboxItem / RadioGroup / RadioItem / ItemIndicator / Separator / Sub / SubTrigger / SubContent

dropdown-menu 章节完全一致(它们本来就是同一组 Base UI Menu 组件):

  • Portal.forceMountkeepMountedcontainer 放宽。
  • ContentlooponCloseAutoFocus、outside/escape 回调、forceMountside/sideOffset/align/alignOffsetavoidCollisionscollisionBoundarycollisionPaddingarrowPaddingstickyhideWhenDetached)→ Menu.Portal > Menu.Positioner > Menu.Popup,归宿与 dropdown-menu Content 的表格逐条相同。
  • SubContentalign 默认 "start" 注意事项同 dropdown-menu。
  • Items:onSelectonClick + closeOnClicktextValuelabel
  • Radix Menubar CheckboxItem/RadioItem 的 [data-state="checked" \| "unchecked"]data-checked/data-unchecked

data 属性 / CSS 变量

  • Menubar 容器:Radix Root 没有任何 data 属性;Base Menubar 暴露 data-orientation'horizontal' \| 'vertical')、data-has-submenu-opendata-modal
  • 菜单 part 的属性与 --radix-menubar-* CSS 变量按 dropdown-menu 两张表映射(Positioner 上的 --transform-origin--available-width/height--anchor-width/height)。

八、navigation-menu:Radix NavigationMenu → Base UI NavigationMenu

Base UI 的 part 集合为:Root, List, Item, Trigger, Icon, Content, Portal, Backdrop, Positioner, Popup, Arrow, Viewport, Link。Popup 定位采用真正的锚定定位模型(与 Menu 相同):共享 popup 渲染为 Portal > Positioner > Popup > Viewport,每个 ItemContent 在激活时被移入 Viewport。Radix“Viewport 渲染在列表下方”的模型被这套锚定 Positioner 模型取代(wrapper 也随之移除了 viewport 布尔 prop)。Base 版 navigation-menu.tsx 中的 NavigationMenuPositioner 完整展示了 Portal > Positioner > Popup > Viewport 四层结构与 --positioner-width/height--popup-width/heightdata-instant 的用法(apps/v4/registry/bases/base/ui/navigation-menu.tsx)。

Root → NavigationMenu.Root

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
defaultValue string / – defaultValueValue | null,默认 null 类型放宽(Value = any);null = 关闭。
value string / – valueValue | null,默认 null 相同;非 nullish = 打开。
onValueChange (value: string) => void / – onValueChange 签名变化:(value: Value | null, eventDetails: NavigationMenu.Root.ChangeEventDetails) => void;reason 取值:'trigger-press' | 'trigger-hover' | 'outside-press' | 'list-navigation' | 'focus-out' | 'escape-key' | 'link-press' | 'none'
delayDuration number / 200 delaynumber,默认 50 改名;默认值从 200 变为 50。
skipDelayDuration number / 300 移除。 没有 skip-delay 窗口;Base 改用 closeDelaynumber,默认 50)。
dir "ltr" | "rtl" / – 移除;用 DirectionProvider
orientation "horizontal" | "vertical" / "horizontal" orientation(取值/默认值相同) 不变。

Base UI Root 独有:closeDelay50)、actionsRef{ unmount() })、onOpenChangeComplete(open)。Root 渲染 <nav> 元素(Radix Root 也渲染 <nav>;嵌套时 Base 渲染 <div>)。

Sub → 嵌套 NavigationMenu.Root

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
defaultValue / value / onValueChange / orientation 同 Root 嵌套 Root 上的同名 props part 移除:在 Content 里嵌套一个完整的 NavigationMenu.Root(带自己的 List/Portal/Positioner/Popup);嵌套时渲染 <div>。与 Radix Sub 不同,嵌套的 Base 菜单默认是关闭的(null),不要求始终有一个活动项。

List / Item / Trigger

part Radix prop 类型 / 默认值 Base UI 对应 迁移说明
ListNavigationMenu.List asChild boolean / false render Base List 渲染 <ul>(Radix 同为 <ul>)。Radix 的 [data-orientation] 属性移除。
ItemNavigationMenu.Item asChild boolean / false render Base Item 渲染 <li>
value string / – valueany 不变;省略时自动生成。
TriggerNavigationMenu.Trigger asChild boolean / false render(另有 nativeButton,默认 true [data-state="open" | "closed"]data-popup-open[data-disabled] 移除(也没有 disabled prop——在 item 层级自行拦截)。

Content → NavigationMenu.Content

Radix prop 类型 / 默认值 Base UI 对应 迁移说明
asChild boolean / false render
onEscapeKeyDown (event: KeyboardEvent) => void / – 移除 → Root onValueChangereason === 'escape-key' + eventDetails.cancel()
onPointerDownOutside (event: PointerDownOutsideEvent) => void / – 移除 → reason === 'outside-press'
onFocusOutside (event: FocusOutsideEvent) => void / – 移除 → reason === 'focus-out'
onInteractOutside (event: PointerDownOutsideEvent | FocusOutsideEvent) => void / – 移除 → 'outside-press' | 'focus-out'
forceMount boolean / – keepMountedboolean,默认 false 改名,仍留在 Content 上(关闭时保留内容在 DOM 中,例如用于 SEO/SSR)。

Link / Indicator / Viewport

part Radix prop 类型 / 默认值 Base UI 对应 迁移说明
LinkNavigationMenu.Link asChild boolean / false render 框架链接:render={<NextLink href=... />}
active boolean / false activeboolean,默认 false 不变(设置 aria-current + data-active)。
onSelect (event: Event) => void / – 移除。改用 onClick(普通 DOM prop)与 closeOnClickboolean,默认 false)——注意 Radix 默认点链接即关闭菜单,Base 不关闭;要行为对齐需显式设置 closeOnClick
IndicatorNavigationMenu.Icon asChild boolean / false render 角色不同:Radix Indicator 在列表下方追踪活动触发器;Base Icon 是 Trigger 内的箭头(其菜单打开时带 data-popup-open)。要做 popup 锚定的指针,Base 的 Arrow(在 Popup 内,带 data-side/data-align/data-uncentered)是最接近的视觉对应物。Base 没有沿列表追踪活动触发器的 part。
forceMount boolean / – 移除(Icon 始终渲染)。
[data-state="visible" | "hidden"][data-orientation] 移除;Icon 只暴露 data-popup-open
ViewportNavigationMenu.Portal > Positioner > Popup > Viewport asChild boolean / false 每个新 part 上的 render 一个 Radix part 变四个:Portal(props:containerkeepMounted)、Positioner(与 Menu.Positioner 完全相同的锚定定位 props 集合:side/sideOffset/align/alignOffsetnumber | OffsetFunction)、anchorcollisionAvoidancecollisionBoundary'clipping-ancestors')、collisionPadding5)、arrowPadding5)、sticky(boolean)、positionMethoddisableAnchorTracking)、Popup(渲染 <nav>)、Viewport(裁剪/动画活动的 Content)。
forceMount boolean / – Portal 上的 keepMounted 改名 + 移动。

Base UI 独有(NavigationMenu):Root 的 closeDelayactionsRefonOpenChangeComplete;新 part BackdropArrowPositioner(真正的碰撞感知定位——Radix nav-menu 没有定位能力)、IconContent.keepMounted 供爬虫可见的 SSR 内容;Link.closeOnClick;所有 part 接受 className/style 状态回调形式与 render

data 属性映射(navigation-menu)

Radix Base UI
Root/Sub/List/Item [data-orientation] 移除。
Trigger [data-state="open" | "closed"] Trigger(以及 Icon)上的 data-popup-open
Trigger [data-disabled] 移除。
Content [data-state="open" | "closed"] Content 上的 data-open / data-closed(Positioner/Popup/Backdrop 上也有)。
Content [data-motion="to-start" | "to-end" | "from-start" | "from-end"] Content 上的 data-activation-direction'left' | 'right' | 'up' | 'down')——新激活触发器相对上一个的方向,用于进出场动画。
Link [data-active] data-active(不变)。
Indicator [data-state="visible" | "hidden"] 无对应物(见 Indicator 行)。Arrow 暴露 data-open/data-closed/data-uncentered/data-side/data-align
Viewport [data-state][data-orientation] Popup/Positioner 上的 data-open/data-closed + data-starting-style/data-ending-style;Viewport 本身不暴露。
Positioner:data-anchor-hiddendata-instant;Popup:data-sidedata-align

仓库 Base 版 wrapper 已用 data-activation-direction + data-starting-style/data-ending-style 重写了切换动画(例如 data-starting-style:data-activation-direction=left:translate-x-[-50%] data-ending-style:opacity-0,见 apps/v4/registry/bases/base/ui/navigation-menu.tsx),替换了 Radix 的 data-motion 方案。

CSS 变量映射(navigation-menu)

Radix Base UI
--radix-navigation-menu-viewport-width(在 Viewport 上) --popup-widthnumber,在 Popup 上)——popup 的固定宽度;动画可用 width: var(--popup-width)
--radix-navigation-menu-viewport-height(在 Viewport 上) --popup-heightnumber,在 Popup 上)。
Positioner 另暴露 --anchor-width--anchor-height--available-width--available-height--positioner-width--positioner-height--transform-origin

九、缺口与注意事项(Gaps / caveats)

以下是原始文档明确记录的边界,迁移时必须如实报告而不是静默“修好”:

  • Radix prop 描述在官方站点以 JS popover 呈现;本文中的类型/默认值均提取自页面内嵌类型载荷((open: boolean) => void(checked: boolean) => void(value: string) => void(event: KeyboardEvent) => void(event: PointerDownOutsideEvent) => void(event: FocusOutsideEvent) => void(event: PointerDownOutsideEvent | FocusOutsideEvent) => voidonSelect/onCloseAutoFocus(event: Event) => voidBoundary = Element | null | Array<Element | null>sticky: "partial" | "always"dir: "ltr" | "rtl"),并对照抓取到的 HTML 验证过。
  • 三处“硬移除”没有一行代码的等价变通:Base UI ContextMenu.Root 确实没有 modal;Menubar 没有 value/onValueChange 体系;ContextMenu.Trigger 没有 disabled
  • Base UI 文档取自 .md 端点,对应 Base UI 1.x;本仓库当前锁定 @base-ui/react 1.6.0(apps/v4/package.json),阅读 props 细节时应以该版本为准。

十、实操建议:结合仓库技能流程迁移

本仓库把这份映射放在一个完整的 Agent 迁移技能里(skills/migrate-radix-to-base/SKILL.md)。迁移菜单组件时的可执行要点:

  1. 先分类 wrapper,再选路径:把用户文件与对应风格的 stock 版本 diff 一下。若是 shadcn 项目且存在成对 registry 版本,优先走“golden pair”(CLI 拉取 base 变体);手写 Radix 组合或未知风格才用本表的转换引擎逐条改写。
  2. 导入改写import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui"import { Menu as MenuPrimitive } from "@base-ui/react/menu";类型写法从 React.ComponentProps<typeof XPrimitive.Part> 改为 XPrimitive.Part.Props(见 universal-patterns.md 的导入规则)。
  3. 结构性重组Content 一律拆为 Portal > Positioner > Popup;定位 props 记得“声明 → 解构 → 转发”三步(见第一节 FORWARD rule 警告);Overlay 场景不涉及菜单,但菜单的 Backdrop 是新 part 可按需使用。
  4. 类字符串层机械改写:按 class-mapping.md 全量替换 data-[state=open]data-opendata-[state=closed]data-closeddata-[state=checked]data-checked、submenu trigger 的 data-[state=open]:data-popup-open:,并把 animate-in/out 动画重写为 data-starting-style:/data-ending-style: transition。
  5. 行为差异要标记而不是修补:Base 的 Menu.Item closeOnClick 语义、CheckboxItem/RadioItem 的 closeOnClick 默认 false、navigation-menu delay 默认 50ms(Radix 200ms)等,编译通过但行为不同的差异应在迁移报告中单列“Behavior changes”一节,供人工 QA 验证(菜单的键盘导航、typeahead、关闭后焦点回归等)。

参考文件汇总:映射主表 menus.md、通用模式与覆盖矩阵 universal-patterns.md、类改写表 class-mapping.md、调用方 props 改写 consumer-props.md、目标形态 wrapper-shapes.md;两侧对照实现 radix/ui/dropdown-menu.tsxbase/ui/dropdown-menu.tsxbase/ui/context-menu.tsxbase/ui/menubar.tsxbase/ui/navigation-menu.tsx

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