migrate-radix-to-base
Migrate React projects and shadcn components from Radix UI to Base UI, handling route objects, consumer props, and progressive wrapper migration.
Install
npx skills add https://github.com/shadcn-ui/ui --skill migrate-radix-to-baseRadix → Base UI props mapping: menu family
Sources: radix-ui.com primitives docs (dropdown-menu, context-menu, menubar, navigation-menu) and base-ui.com /react/components/{menu,context-menu,menubar,navigation-menu}.md, fetched 2026-07-02.
Part-mapping ground truth (from our wrappers): Content → Portal > Positioner > Popup (side/sideOffset/align/alignOffset live on Positioner); Label → GroupLabel; ItemIndicator → CheckboxItemIndicator/RadioItemIndicator; Sub → SubmenuRoot; SubTrigger → SubmenuTrigger; navigation-menu Viewport → Positioner > Popup > Viewport, Indicator → Icon; asChild → render.
Cross-cutting rules (apply to every part below):
dropdown-menu (Radix DropdownMenu → Base UI Menu)
Root → Menu.Root
Trigger → Menu.Trigger
Portal → Menu.Portal
Content → Menu.Portal > Menu.Positioner > Menu.Popup
Arrow → Menu.Arrow
Item → Menu.Item
Group → Menu.Group
Label → Menu.GroupLabel
CheckboxItem → Menu.CheckboxItem
RadioGroup → Menu.RadioGroup
RadioItem → Menu.RadioItem
ItemIndicator → Menu.CheckboxItemIndicator / Menu.RadioItemIndicator
Separator → Menu.Separator
Sub → Menu.SubmenuRoot
SubTrigger → Menu.SubmenuTrigger
SubContent → Menu.Portal > Menu.Positioner > Menu.Popup (inside SubmenuRoot)
Same prop fates as Content above; Radix-specific defaults to be aware of: align default is "start" on SubContent (Base Positioner default is 'center' — set align="start" explicitly if you relied on the Radix default; in practice submenu popups anchor to the trigger item and our wrappers set this). Radix SubContent has no side prop (side is implied); Base Positioner accepts side (use 'inline-end' for RTL-aware submenus). All outside/escape callbacks, forceMount, loop, collision props map identically to Content.
Base UI only props worth knowing (Menu)
Root:highlightItemOnHover(true),actionsRef({ unmount(), close() }),onOpenChangeComplete(open)(fires after close animation; replaces the Radix "wait for animation" dance),closeParentOnEsc(false),disabled,orientation('vertical'), detached-trigger machinery:handle(Menu.HandleviaMenu.createHandle()),triggerId/defaultTriggerId, payload-awarechildrenrender function.Trigger:openOnHover,delay(100),closeDelay(0),payload,handle,nativeButton(true).Backdrop: new part, overlay under the popup.Positioner:anchor,positionMethod('absolute'),disableAnchorTracking,sticky(boolean, viewport-keeping).Popup:finalFocus.Viewport: new part for animating content swaps with multiple/detached triggers.LinkItem: new part,<a>-rendering menu item (closeOnClickdefaultfalse).- All parts:
className/styleaccept state-callback form(state) => ....
Data-attribute mapping (dropdown/context/menubar menus)
CSS variable mapping (per menu flavor: dropdown-menu / context-menu / menubar)
Base UI Menu.Viewport additionally exposes --popup-width / --popup-height (previous-content dimensions during transitions).
context-menu (Radix ContextMenu → Base UI ContextMenu)
Base UI ContextMenu shares the Menu part set: Root, Trigger, Portal, Backdrop, Positioner, Popup, Arrow, Item, Group, GroupLabel, Separator, SubmenuRoot, SubmenuTrigger, RadioGroup, RadioItem, RadioItemIndicator, CheckboxItem, CheckboxItemIndicator, LinkItem. Everything not listed below maps exactly as in the dropdown-menu section.
Root → ContextMenu.Root
Trigger → ContextMenu.Trigger
Portal → ContextMenu.Portal
Identical mapping to Menu.Portal (forceMount → keepMounted, container widened).
Content → ContextMenu.Portal > Positioner > Popup
Same fates as dropdown-menu Content for: asChild, loop (→ Root loopFocus), onCloseAutoFocus (→ Popup finalFocus), onEscapeKeyDown/onPointerDownOutside/onFocusOutside/onInteractOutside (→ Root onOpenChange reasons), forceMount (→ Portal keepMounted), avoidCollisions (→ collisionAvoidance), collisionBoundary (default [] → 'clipping-ancestors'), collisionPadding (0 → 5), sticky (dropped, see menu note), hideWhenDetached (→ CSS on data-anchor-hidden).
Radix-specific deltas:
Arrow / Item / Group / Label / CheckboxItem / RadioGroup / RadioItem / ItemIndicator / Separator / Sub / SubTrigger / SubContent
Identical fates to the dropdown-menu section (Label → GroupLabel, ItemIndicator → CheckboxItemIndicator/RadioItemIndicator, Sub → SubmenuRoot, SubTrigger → SubmenuTrigger, SubContent → Portal > Positioner > Popup). Menubar/ContextMenu submenus in Base are the same components with the same props (onOpenChange eventDetails, label, onClick/closeOnClick, keepMounted).
Base UI only, data attributes, CSS variables
Same as the Menu lists above; ContextMenu.Root additionally supports handle (MenuHandle<unknown>), triggerId/defaultTriggerId, actionsRef, onOpenChangeComplete, highlightItemOnHover, closeParentOnEsc, disabled, orientation. Trigger data attributes: data-popup-open, data-pressed (replacing Radix Trigger [data-state]). CSS vars: --radix-context-menu-* → --transform-origin/--available-*/--anchor-* on Positioner.
menubar (Radix Menubar → Base UI Menubar + Menu)
Base UI's menubar module exports a single <Menubar> container. Every menu inside it is built from Menu.* parts (Menu.Root, Menu.Trigger, Menu.Portal, Menu.Positioner, Menu.Popup, items, submenus...). So the Radix Menubar.Menu/Trigger/Portal/Content/... parts all map to the Menu component family from the dropdown-menu section.
Root → Menubar
Base UI only on Menubar: modal (boolean, true), disabled (boolean, false), orientation ('horizontal' \| 'vertical', 'horizontal').
Menu → Menu.Root
Note: Menu.Root inside a Menubar accepts all Menu.Root props (open, defaultOpen, onOpenChange(open, eventDetails), modal, loopFocus, orientation, disabled, ...). Hover-switching between menubar menus is built in.
Trigger → Menu.Trigger
Portal / Content / Arrow / Item / Group / Label / CheckboxItem / RadioGroup / RadioItem / ItemIndicator / Separator / Sub / SubTrigger / SubContent
All identical to the dropdown-menu section (they are literally the same Base UI Menu components):
Portal.forceMount→keepMounted;containerwidened.Content(loop,onCloseAutoFocus, outside/escape callbacks,forceMount,side/sideOffset/align/alignOffset,avoidCollisions,collisionBoundary,collisionPadding,arrowPadding,sticky,hideWhenDetached) →Menu.Portal > Menu.Positioner > Menu.Popupwith the exact fates listed for dropdown-menu Content.SubContentaligndefault"start"note applies as in dropdown-menu.- Items:
onSelect→onClick+closeOnClick,textValue→label. - Radix Menubar CheckboxItem/RadioItem
[data-state="checked" \| "unchecked"]→data-checked/data-unchecked.
Data attributes / CSS variables
- Menubar container: Radix Root had none; Base
Menubarexposesdata-orientation('horizontal' \| 'vertical'),data-has-submenu-open,data-modal. - Menu-part attributes and
--radix-menubar-*CSS vars map exactly as in the dropdown-menu tables (--transform-origin,--available-width/height,--anchor-width/heightonMenu.Positioner).
navigation-menu (Radix NavigationMenu → Base UI NavigationMenu)
Base UI parts: Root, List, Item, Trigger, Icon, Content, Portal, Backdrop, Positioner, Popup, Arrow, Viewport, Link. Popup positioning is real anchored positioning (like Menu): the shared popup renders as Portal > Positioner > Popup > Viewport, and each Item's Content is moved into the Viewport when active. Radix's "Viewport rendered below the list" model is replaced by this anchored Positioner model (our wrappers removed the viewport boolean prop accordingly).
Root → NavigationMenu.Root
Base UI only on Root: closeDelay (50), actionsRef ({ unmount() }), onOpenChangeComplete(open). Root renders a <nav> element (Radix Root also rendered <nav>; Base renders <div> when nested).
Sub → nested NavigationMenu.Root
List → NavigationMenu.List
Item → NavigationMenu.Item
Trigger → NavigationMenu.Trigger
Content → NavigationMenu.Content
Link → NavigationMenu.Link
Indicator → NavigationMenu.Icon (per wrapper ground truth)
Viewport → NavigationMenu.Portal > Positioner > Popup > Viewport
Base UI only props worth knowing (NavigationMenu)
- Root:
closeDelay,actionsRef,onOpenChangeComplete. - New parts:
Backdrop,Arrow,Positioner(real collision-aware positioning — Radix nav-menu had none),Icon. Content.keepMountedfor crawler-visible SSR content.Link.closeOnClick.- All parts accept
className/stylestate-callback forms andrender.
Data-attribute mapping (navigation-menu)
CSS variable mapping (navigation-menu)
Gaps / caveats
- Radix prop descriptions are rendered in JS popovers; types/defaults above were extracted from the pages' embedded type payloads (
(open: boolean) => void,(checked: boolean) => void,(value: string) => void,(event: KeyboardEvent) => void,(event: PointerDownOutsideEvent) => void,(event: FocusOutsideEvent) => void,(event: PointerDownOutsideEvent | FocusOutsideEvent) => void,(event: Event) => voidforonSelect/onCloseAutoFocus,Boundary = Element | null | Array<Element | null>,sticky: "partial" | "always",dir: "ltr" | "rtl") — all verified against the fetched HTML. - Base UI ContextMenu.Root genuinely lacks
modal; Menubar lacks the value/onValueChange system; ContextMenu.Trigger lacksdisabled. These are the three hard drops with no one-line workaround. - Base UI docs fetched from the
.mdendpoints reflect Base UI 1.x (repo currently pins 1.6.0).