Deft
Ask AI

Search

Search skills, authors and topics

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-base

Radix → 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):

Radix patternBase UI equivalent
asChild (boolean, false)render (ReactElement | ((props: HTMLProps, state) => ReactElement)). No merge-onto-child boolean; pass the element or a function.
dir ("ltr" | "rtl") on rootsDropped everywhere. Base UI reads direction from <DirectionProvider> (@base-ui-components/react/direction-provider) or the DOM dir attribute.
forceMount (boolean)keepMounted (boolean, false) on Portal / indicator parts. Same use case (animation/SEO), presence is CSS-driven via data-starting-style / data-ending-style instead of Radix data-state + forced mount.
onEscapeKeyDown / onPointerDownOutside / onFocusOutside / onInteractOutside (content parts)Dropped as separate props. Use onOpenChange(open, eventDetails) on the Root and branch on eventDetails.reason ('escape-key', 'outside-press', 'focus-out', ...). Call eventDetails.cancel() to prevent the close (replaces event.preventDefault()).
onSelect on items ((event: Event) => void; event.preventDefault() keeps menu open)onClick ((event: BaseUIEvent<React.MouseEvent<HTMLDivElement>>) => void) plus closeOnClick (boolean) to control whether the menu closes.
textValue on items (string, typeahead)label (string).
Controlled callbacks (value) => voidAll Base UI change callbacks take a second eventDetails argument ({ reason, event, cancel(), allowPropagation(), isCanceled, isPropagationAllowed, trigger }).

dropdown-menu (Radix DropdownMenu → Base UI Menu)

Root → Menu.Root

Radix propType / defaultBase UI equivalentMigration note
defaultOpenboolean / –defaultOpen (boolean, false)Same.
openboolean / –open (boolean)Same.
onOpenChange(open: boolean) => void / –onOpenChangeSignature changed: (open: boolean, eventDetails: Menu.Root.ChangeEventDetails) => void. eventDetails.reason is one of '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() blocks the state change.
modalboolean / truemodal (boolean, true)Same.
dir"ltr" | "rtl" / ––Dropped. Use DirectionProvider.

Trigger → Menu.Trigger

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSee cross-cutting rules. When rendering a non-button, also set nativeButton={false}.

Portal → Menu.Portal

Radix propType / defaultBase UI equivalentMigration note
forceMountboolean / –keepMounted (boolean, false)Renamed; keeps portal in DOM while hidden.
containerHTMLElement / document.bodycontainer (HTMLElement | ShadowRoot | React.RefObject<HTMLElement | ShadowRoot | null> | null)Same, wider type (accepts refs and ShadowRoot).

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

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender on PopupSee cross-cutting rules.
loopboolean / falseloopFocus on Root (boolean, true)Moved + renamed. Default flips: Base UI loops by default.
onCloseAutoFocus(event: Event) => void / –finalFocus on PopupSignature changed: boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | void | HTMLElement | null) where InteractionType = 'mouse' | 'touch' | 'pen' | 'keyboard'. Return false to replicate event.preventDefault(); return an element to redirect focus.
onEscapeKeyDown(event: KeyboardEvent) => void / ––Dropped → onOpenChange with reason === 'escape-key'.
onPointerDownOutside(event: PointerDownOutsideEvent) => void / ––Dropped → onOpenChange with reason === 'outside-press'.
onFocusOutside(event: FocusOutsideEvent) => void / ––Dropped → onOpenChange with reason === 'focus-out'.
onInteractOutside(event: PointerDownOutsideEvent | FocusOutsideEvent) => void / ––Dropped → onOpenChange with reason === 'outside-press' || 'focus-out'.
forceMountboolean / –keepMounted on PortalMoved; animate with data-starting-style/data-ending-style.
side"top" | "right" | "bottom" | "left" / "bottom"side on Positioner (Side, 'bottom')Moved. Base adds logical values: Side = 'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start'.
sideOffsetnumber / 0sideOffset on Positioner (number | OffsetFunction, 0)Moved; also accepts (data: { side, align, anchor: {width,height}, positioner: {width,height} }) => number.
align"start" | "center" | "end" / "center"align on Positioner (Align, 'center')Moved, same values/default.
alignOffsetnumber / 0alignOffset on Positioner (number | OffsetFunction, 0)Moved; also accepts function form.
avoidCollisionsboolean / truecollisionAvoidance on Positioner (CollisionAvoidance)Signature changed: object { side?: 'flip' | 'shift' | 'none'; align?: 'flip' | 'shift' | 'none'; fallbackAxisSide?: 'start' | 'end' | 'none' }. avoidCollisions={false} → collisionAvoidance={{ side: 'none', align: 'none', fallbackAxisSide: 'none' }}.
collisionBoundaryElement | null | Array<Element | null> / []collisionBoundary on Positioner (Boundary, 'clipping-ancestors')Default changes: Radix [] means viewport/clipping ancestors; Base default 'clipping-ancestors' is equivalent. Also accepts an element or rect.
collisionPaddingnumber | Padding / 0collisionPadding on Positioner (Padding, 5)Same shape; default changes 0 → 5.
arrowPaddingnumber / 0arrowPadding on Positioner (number, 5)Same; default changes 0 → 5.
sticky"partial" | "always" / "partial"– (see note)Different concept. Radix sticky controls align-axis sticking; closest Base knob is collisionAvoidance.align ('shift' ≈ partial). Base UI's own sticky (boolean, false) instead keeps the popup in the viewport after the anchor scrolls away, which has no Radix equivalent.
hideWhenDetachedboolean / false–Dropped as behavior prop. Base always exposes data-anchor-hidden on Positioner/Popup; hide via CSS: [data-anchor-hidden] { visibility: hidden }.

Arrow → Menu.Arrow

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderBase Arrow renders a <div> you fill with an SVG (Radix renders the svg itself). Place inside Popup.
widthnumber / 10–Dropped; size the child SVG/element with CSS.
heightnumber / 5–Dropped; size with CSS.

Item → Menu.Item

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderFor links use Menu.LinkItem (renders <a>) instead of render.
disabledboolean / –disabled (boolean, false)Same.
onSelect(event: Event) => void / –onClick ((event: BaseUIEvent<React.MouseEvent<HTMLDivElement>>) => void)Renamed + signature changed. event.preventDefault() in onSelect (keep open) → closeOnClick={false} (boolean, default true on Item).
textValuestring / –label (string)Renamed.

Group → Menu.Group

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSame otherwise.

Label → Menu.GroupLabel

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderPart renamed. Base GroupLabel must be inside a Group (it wires aria-labelledby); Radix Label could float freely.

CheckboxItem → Menu.CheckboxItem

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender–
checkedboolean | 'indeterminate' / –checked (boolean)'indeterminate' dropped. Base adds defaultChecked (boolean, false) for uncontrolled use.
onCheckedChange(checked: boolean) => void / –onCheckedChangeSignature changed: (checked: boolean, eventDetails: Menu.CheckboxItem.ChangeEventDetails) => void.
disabledboolean / –disabled (boolean, false)Same.
onSelect(event: Event) => void / –onClick + closeOnClickBehavior default flips: Radix closes on select (unless prevented); Base closeOnClick defaults to false on CheckboxItem. Set closeOnClick explicitly to preserve Radix behavior.
textValuestring / –labelRenamed.

RadioGroup → Menu.RadioGroup

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender–
valuestring / –value (any)Type widens to any. Base adds defaultValue (any) and disabled (boolean, false).
onValueChange(value: string) => void / –onValueChangeSignature changed: (value: any, eventDetails: Menu.RadioGroup.ChangeEventDetails) => void.

RadioItem → Menu.RadioItem

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender–
value*string / –value* (any)Same (required); type widens.
disabledboolean / –disabled (boolean, false)Same.
onSelect(event: Event) => void / –onClick + closeOnClickcloseOnClick defaults to false on RadioItem (Radix closed by default).
textValuestring / –labelRenamed.

ItemIndicator → Menu.CheckboxItemIndicator / Menu.RadioItemIndicator

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderPart splits: use the indicator matching the parent item type. Renders <span>.
forceMountboolean / –keepMounted (boolean, false)Renamed.

Separator → Menu.Separator

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderBase adds orientation ('horizontal' | 'vertical', 'horizontal').
Radix propType / defaultBase UI equivalentMigration note
defaultOpenboolean / –defaultOpen (boolean, false)Same.
openboolean / –open (boolean)Same.
onOpenChange(open: boolean) => void / –onOpenChangeSignature changed: (open: boolean, eventDetails: Menu.SubmenuRoot.ChangeEventDetails) => void (same reason union as Root).

SubTrigger → Menu.SubmenuTrigger

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderRenders <div>; nativeButton defaults false here.
disabledboolean / –disabled (boolean, false)Same.
textValuestring / –labelRenamed. Base adds openOnHover / delay (100) / closeDelay (0) and onClick.

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.Handle via Menu.createHandle()), triggerId/defaultTriggerId, payload-aware children render 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 (closeOnClick default false).
  • All parts: className/style accept state-callback form (state) => ....

Data-attribute mapping (dropdown/context/menubar menus)

RadixBase UI
Trigger [data-state="open" | "closed"]data-popup-open (presence) + data-pressed
Content [data-state="open" | "closed"]data-open / data-closed on Positioner and Popup
–data-starting-style / data-ending-style (CSS transition hooks, replace animating on data-state)
Content [data-side="left" | "right" | "bottom" | "top"]data-side ('top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start') on Positioner/Popup/Arrow
Content [data-align="start" | "end" | "center"]data-align (same values) on Positioner/Popup/Arrow
Content/Item [data-orientation]Dropped on menu parts
Item [data-highlighted]data-highlighted (same)
Item [data-disabled]data-disabled (same)
Checkbox/RadioItem [data-state="checked" | "unchecked" | "indeterminate"]data-checked / data-unchecked presence attrs; no indeterminate
ItemIndicator [data-state]data-checked / data-unchecked + data-starting-style / data-ending-style on the split indicators
SubTrigger [data-state="open" | "closed"]data-popup-open on SubmenuTrigger
–Popup data-instant ('click' | 'dismiss' | 'group' | 'trigger-change'), Positioner data-anchor-hidden

CSS variable mapping (per menu flavor: dropdown-menu / context-menu / menubar)

Radix (on Content/SubContent)Base UI (on 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 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

Radix propType / defaultBase UI equivalentMigration note
dir"ltr" | "rtl" / ––Dropped; DirectionProvider.
openboolean / –open (boolean)Same. Base also adds defaultOpen (false), which Radix ContextMenu lacked.
onOpenChange(open: boolean) => void / –onOpenChangeSignature changed: (open: boolean, eventDetails: ContextMenu.Root.ChangeEventDetails) => void (same reason union as Menu).
modalboolean / true–Dropped. Base UI ContextMenu.Root has no modal prop (behavior is fixed). If you relied on modal={false}, there is no direct equivalent.

Trigger → ContextMenu.Trigger

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderBase Trigger renders a <div> (also handles long-press on touch).
disabledboolean / false–Dropped. ContextMenu.Trigger has only className/style/render. Workaround: conditionally render content outside the Trigger, or intercept onContextMenu with preventDefault + stopPropagation on a child.

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:

Radix propType / defaultBase UI equivalentMigration note
alignOffsetnumber / 0alignOffset on Positioner (number | OffsetFunction, 0)Radix ContextMenu.Content has no side/sideOffset/align props (anchored to pointer). Base Positioner still accepts side/align/sideOffset but anchors to the pointer position by default; usually leave them off.
(no arrowPadding on Content)–arrowPadding (5) on PositionerAvailable in Base if you add an Arrow.

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

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSame pattern.
defaultValuestring / ––Dropped. Base Menubar has no controlled/uncontrolled active-menu value. To pre-open a menu, use defaultOpen on that Menu.Root.
valuestring / ––Dropped. Control individual Menu.Root open props instead.
onValueChange(value: string) => void / ––Dropped. Listen via each Menu.Root onOpenChange.
dir"ltr" | "rtl" / ––Dropped; DirectionProvider.
loopboolean / falseloopFocus (boolean, true)Renamed; default flips to true.

Base UI only on Menubar: modal (boolean, true), disabled (boolean, false), orientation ('horizontal' \| 'vertical', 'horizontal').

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / false–Menu.Root renders no element; drop it.
valuestring / ––Dropped with the Menubar value system (see 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

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender (+ nativeButton={false} for non-buttons)Radix Menubar.Trigger data attrs [data-state]/[data-highlighted]/[data-disabled] → data-popup-open/data-pressed (no highlighted state on Base 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; container widened.
  • Content (loop, onCloseAutoFocus, outside/escape callbacks, forceMount, side/sideOffset/align/alignOffset, avoidCollisions, collisionBoundary, collisionPadding, arrowPadding, sticky, hideWhenDetached) → Menu.Portal > Menu.Positioner > Menu.Popup with the exact fates listed for dropdown-menu Content.
  • SubContent align default "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 Menubar exposes data-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/height on Menu.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

Radix propType / defaultBase UI equivalentMigration note
defaultValuestring / –defaultValue (Value | null, null)Type widens (Value = any); null = closed.
valuestring / –value (Value | null, null)Same; non-nullish = open.
onValueChange(value: string) => void / –onValueChangeSignature changed: (value: Value | null, eventDetails: NavigationMenu.Root.ChangeEventDetails) => void; reasons: 'trigger-press' | 'trigger-hover' | 'outside-press' | 'list-navigation' | 'focus-out' | 'escape-key' | 'link-press' | 'none'.
delayDurationnumber / 200delay (number, 50)Renamed; default 200 → 50.
skipDelayDurationnumber / 300–Dropped. No skip-delay window; Base instead has closeDelay (number, 50).
dir"ltr" | "rtl" / ––Dropped; DirectionProvider.
orientation"horizontal" | "vertical" / "horizontal"orientation (same values/default)Same.

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

Radix propType / defaultBase UI equivalentMigration note
defaultValue / value / onValueChange / orientationas Rootsame props on the nested RootPart dropped: nest a whole NavigationMenu.Root (with its own List/Portal/Positioner/Popup) inside a Content; it renders a <div> when nested. Unlike Radix Sub, a nested Base menu is closed by default (null), not required to always have an active item.

List → NavigationMenu.List

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderBase List renders <ul> (Radix also <ul>). Radix [data-orientation] attr dropped.

Item → NavigationMenu.Item

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderBase Item renders <li>.
valuestring / –value (any)Same; auto-generated if omitted.

Trigger → NavigationMenu.Trigger

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender (+ nativeButton, default true)[data-state="open" | "closed"] → data-popup-open; [data-disabled] dropped (no disabled prop either — gate at the item level yourself).

Content → NavigationMenu.Content

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender–
onEscapeKeyDown(event: KeyboardEvent) => void / ––Dropped → Root onValueChange with reason === 'escape-key' + eventDetails.cancel().
onPointerDownOutside(event: PointerDownOutsideEvent) => void / ––Dropped → reason === 'outside-press'.
onFocusOutside(event: FocusOutsideEvent) => void / ––Dropped → reason === 'focus-out'.
onInteractOutside(event: PointerDownOutsideEvent | FocusOutsideEvent) => void / ––Dropped → 'outside-press' | 'focus-out'.
forceMountboolean / –keepMounted (boolean, false)Renamed, stays on Content (keeps content in DOM while closed, e.g. for SEO/SSR).
Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderFor framework links: render={<NextLink href=... />}.
activeboolean / falseactive (boolean, false)Same (sets aria-current + data-active).
onSelect(event: Event) => void / ––Dropped. Use onClick (plain DOM prop) and closeOnClick (boolean, false) — note Radix closed the menu on link select by default, Base does not; set closeOnClick for parity.

Indicator → NavigationMenu.Icon (per wrapper ground truth)

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderDifferent role: Radix Indicator tracked the active trigger below the List; Base Icon is a chevron inside the Trigger (data-popup-open when its menu is open). For a popup-anchored pointer, Base's Arrow (inside Popup, with data-side/data-align/data-uncentered) is the closest visual analogue. There is no Base part that tracks the active trigger along the list.
forceMountboolean / ––Dropped (Icon is always rendered).
[data-state="visible" | "hidden"], [data-orientation]––Dropped; Icon exposes only data-popup-open.

Viewport → NavigationMenu.Portal > Positioner > Popup > Viewport

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender on each new partOne Radix part becomes four: Portal (props: container, keepMounted), Positioner (full anchored-positioning prop set identical to Menu.Positioner: side/sideOffset/align/alignOffset (number | OffsetFunction), anchor, collisionAvoidance, collisionBoundary 'clipping-ancestors', collisionPadding 5, arrowPadding 5, sticky boolean, positionMethod, disableAnchorTracking), Popup (renders <nav>), Viewport (clips/animates the active Content).
forceMountboolean / –keepMounted on PortalRenamed + moved.

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.keepMounted for crawler-visible SSR content.
  • Link.closeOnClick.
  • All parts accept className/style state-callback forms and render.

Data-attribute mapping (navigation-menu)

RadixBase UI
Root/Sub/List/Item [data-orientation]Dropped.
Trigger [data-state="open" | "closed"]data-popup-open on Trigger (and on Icon).
Trigger [data-disabled]Dropped.
Content [data-state="open" | "closed"]data-open / data-closed on Content (also on Positioner/Popup/Backdrop).
Content [data-motion="to-start" | "to-end" | "from-start" | "from-end"]data-activation-direction ('left' | 'right' | 'up' | 'down') on Content — direction the newly-activated trigger is relative to the previous one; use for enter/exit animations.
Link [data-active]data-active (same).
Indicator [data-state="visible" | "hidden"]No equivalent (see Indicator row). Arrow exposes data-open/data-closed/data-uncentered/data-side/data-align.
Viewport [data-state], [data-orientation]data-open/data-closed + data-starting-style/data-ending-style on Popup/Positioner; Viewport itself exposes none.
–Positioner: data-anchor-hidden, data-instant; Popup: data-side, data-align.

CSS variable mapping (navigation-menu)

RadixBase UI
--radix-navigation-menu-viewport-width (on Viewport)--popup-width (number, on Popup) — fixed width of the popup; animate width: var(--popup-width).
--radix-navigation-menu-viewport-height (on Viewport)--popup-height (number, on Popup).
–Positioner also exposes --anchor-width, --anchor-height, --available-width, --available-height, --positioner-width, --positioner-height, --transform-origin.

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) => void for onSelect/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 lacks disabled. These are the three hard drops with no one-line workaround.
  • Base UI docs fetched from the .md endpoints reflect Base UI 1.x (repo currently pins 1.6.0).