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: disclosure + toggle family

Scope: accordion, collapsible, tabs, toggle, toggle-group, toolbar. Sources: radix-ui.com primitives docs + base-ui.com .md docs, cross-checked against installed @base-ui/react@1.6.0 .d.ts files (the published docs page for accordion lagged; types are authoritative here).

Conventions that apply to every component below:

  • asChild (boolean, default false) → render (ReactElement | (props: HTMLProps, state) => ReactElement). Signature changed: instead of a lone child element, pass the element to render; Base UI merges props onto it. Button-rendering parts additionally accept nativeButton (default true), set it to false when render produces a non-<button> element.
  • Base UI className and style also accept a (state) => value function form.
  • Radix data-[state=...] value attributes become Base UI presence attributes (data-open, data-closed, data-pressed, data-active).
  • Base UI change callbacks all gained a second eventDetails argument ({ reason, event, cancel(), ... }).
  • Radix dir props are dropped everywhere; Base UI reads direction from the DOM dir attribute / DirectionProvider.

accordion

Part mapping: Root → Root, Item → Item, Header → Header, Trigger → Trigger, Content → Panel.

Accordion.Root → Accordion.Root

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed. Pass element to render instead of wrapping a child.
type (required)"single" | "multiple" / —multipleSignature changed. type="multiple" → multiple (boolean, default false); type="single" → omit.
valuestring (single) or string[] (multiple) / —valueSignature changed. Base UI is ALWAYS an array (Value[], Value = any), even in single mode: value="a" → value={["a"]}.
defaultValuestring or string[] / —defaultValueSame array caveat as value.
onValueChange(value: string) => void or (value: string[]) => void / —onValueChangeSignature changed: (value: Value[], eventDetails: Accordion.Root.ChangeEventDetails) => void. Always receives an array; unwrap value[0] for single mode.
collapsibleboolean / false— droppedBase UI single mode is always collapsible. To forbid closing the last open item (Radix collapsible={false} default), control value and ignore updates where the array is empty, or call eventDetails.cancel() when value.length === 0.
disabledboolean / falsedisabled (default false)Same.
dir"ltr" | "rtl" / "ltr"— droppedUse DOM dir attribute / DirectionProvider.
orientation"vertical" | "horizontal" / "vertical"— dropped (prop exists but deprecated no-op)Base UI removed roving arrow-key focus per the APG guidance update, so orientation (and loopFocus) no longer affect keyboard behavior. Do not carry it over.

Accordion.Item → Accordion.Item

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed.
value (required)string / —valueRenamed constraint: Base UI value is any and OPTIONAL (auto-generated from index when omitted). Keep passing strings for parity.
disabledboolean / falsedisabled (default false)Same.

Accordion.Header → Accordion.Header

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed. Both render <h3> by default.

Accordion.Trigger → Accordion.Trigger

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender (+ nativeButton, default true)Signature changed.

Accordion.Content → Accordion.Panel

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed.
forceMounttrue | undefined / —keepMounted (boolean, default false)Renamed. forceMount → keepMounted (closed panel stays in DOM, hidden). Also available on Root to apply to all panels.

Base UI only props worth knowing

  • Root.hiddenUntilFound / Panel.hiddenUntilFound (default false): uses hidden="until-found" so browser find-in-page can expand panels; overrides keepMounted. No Radix equivalent.
  • Root.keepMounted: root-level version of the per-panel prop.
  • Item.onOpenChange: (open: boolean, eventDetails: Accordion.Item.ChangeEventDetails) => void, per-item open callback. No Radix equivalent.
  • Trigger.nativeButton (default true).
  • className / style state-function forms on every part.

Data-attribute mapping

RadixBase UINote
Item/Header/Content [data-state="open" | "closed"]Item, Header: data-open (presence); Panel: data-open (presence)No data-closed on accordion parts (unlike collapsible); style closed state as the absence of data-open.
Trigger [data-state="open"]Trigger [data-panel-open]Renamed. Trigger specifically uses data-panel-open, NOT data-open.
[data-disabled][data-disabled]Same (Root, Item, Header, Trigger, Panel).
[data-orientation] (all parts)Root, Panel: data-orientationDeprecated along with orientation; avoid relying on it.
—Item/Header/Panel [data-index]Base UI only: numeric item index.
—Panel [data-starting-style], [data-ending-style]Base UI only: CSS-transition animation hooks (replace Radix mount/unmount animation pattern).

CSS var mapping

RadixBase UI
--radix-accordion-content-height--accordion-panel-height
--radix-accordion-content-width--accordion-panel-width

collapsible

Part mapping: Root → Root, Trigger → Trigger, Content → Panel.

Collapsible.Root → Collapsible.Root

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed.
defaultOpenboolean / —defaultOpen (default false)Same.
openboolean / —openSame.
onOpenChange(open: boolean) => void / —onOpenChangeSignature changed: (open: boolean, eventDetails: Collapsible.Root.ChangeEventDetails) => void.
disabledboolean / —disabled (default false)Same.

Collapsible.Trigger → Collapsible.Trigger

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender (+ nativeButton, default true)Signature changed.

Collapsible.Content → Collapsible.Panel

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed.
forceMounttrue | undefined / —keepMounted (boolean, default false)Renamed.

Base UI only props worth knowing

  • Panel.hiddenUntilFound (default false): find-in-page support via hidden="until-found"; overrides keepMounted.
  • Trigger.nativeButton (default true).
  • className / style state-function forms.

Data-attribute mapping

RadixBase UINote
Root/Content [data-state="open" | "closed"]Panel [data-open] / [data-closed]Renamed to presence attributes. Base UI Root renders a plain <div>; state attrs live on Panel/Trigger.
Trigger [data-state="open"]Trigger [data-panel-open]Renamed; trigger-specific name.
[data-disabled]— (not emitted on collapsible parts)Gate styles on the disabled prop / :disabled on the trigger instead.
—Panel [data-starting-style], [data-ending-style]Base UI only: animation hooks.

CSS var mapping

RadixBase UI
--radix-collapsible-content-height--collapsible-panel-height
--radix-collapsible-content-width--collapsible-panel-width

tabs

Part mapping: Root → Root, List → List, Trigger → Tab, Content → Panel. Base UI adds an Indicator part with no Radix equivalent.

Tabs.Root → Tabs.Root

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed.
defaultValuestring / —defaultValueSignature changed: Base UI value type is Tabs.Tab.Value (any), default 0 (first tab active by default; Radix has no default active tab). Strings still work unchanged.
valuestring / —valueSame shape for string values; type widened to any.
onValueChange(value: string) => void / —onValueChangeSignature changed: (value: Tabs.Tab.Value, eventDetails: Tabs.Root.ChangeEventDetails) => void.
orientation"horizontal" | "vertical" / "horizontal"orientation (default 'horizontal')Same.
dir"ltr" | "rtl" / —— droppedUse DOM dir / DirectionProvider.
activationMode"automatic" | "manual" / "automatic"moved + renamed: List.activateOnFocus (boolean, default false)Moved from Root to List and inverted DEFAULT: Radix defaults to automatic, Base UI 1.6.0 defaults to false (manual). To preserve Radix default behavior set <Tabs.List activateOnFocus>; activationMode="manual" → omit.

Tabs.List → Tabs.List

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed.
loopboolean / trueloopFocus (default true)Renamed.

Tabs.Trigger → Tabs.Tab

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender (+ nativeButton, default true)Signature changed.
value (required)string / —value (required)Type widened to Tabs.Tab.Value (any); strings unchanged.
disabledboolean / falsedisabledSame.

Tabs.Content → Tabs.Panel

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed.
value (required)string / —value (required)Type widened; strings unchanged.
forceMounttrue | undefined / —keepMounted (boolean, default false)Renamed. Hidden panels stay in DOM with data-hidden.

Base UI only props worth knowing

  • Tabs.Indicator: new part, a <span> that tracks the active tab for sliding-highlight UIs; renderBeforeHydration (default false) for SSR-flash avoidance. Exposes the --active-tab-* CSS vars below.
  • List.activateOnFocus (see above).
  • Tab.nativeButton, state-function className/style on all parts.

Data-attribute mapping

RadixBase UINote
Trigger [data-state="active" | "inactive"]Tab [data-active] (presence)Renamed. Inactive = absence of data-active.
Content [data-state="active" | "inactive"]Panel [data-hidden] (presence when hidden)Inverted polarity: Radix marks the active state, Base UI marks the hidden state.
[data-orientation] (all parts)[data-orientation] (Root, List, Tab, Panel, Indicator)Same.
Trigger [data-disabled]Tab [data-disabled]Same.
—[data-activation-direction] ('left' | 'right' | 'up' | 'down' | 'none', all parts)Base UI only: direction of the last tab change, useful for directional animations.
—Panel [data-index], [data-starting-style], [data-ending-style]Base UI only.

CSS var mapping

Radix Tabs exposes no CSS variables. Base UI only (on Indicator): --active-tab-left, --active-tab-right, --active-tab-top, --active-tab-bottom, --active-tab-width, --active-tab-height.


toggle

Part mapping: Toggle.Root → Toggle (single-part; Base UI export is directly callable, no .Root).

Toggle.Root → Toggle

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender (+ nativeButton, default true)Signature changed.
defaultPressedboolean / —defaultPressed (default false)Same.
pressedboolean / —pressedSame.
onPressedChange(pressed: boolean) => void / —onPressedChangeSignature changed: (pressed: boolean, eventDetails: Toggle.ChangeEventDetails) => void.
disabledboolean / —disabled (default false)Same.

Base UI only props worth knowing

  • value?: string: identifies the toggle inside a Base UI ToggleGroup (this replaces Radix ToggleGroup.Item's value, see toggle-group below).
  • nativeButton (default true), state-function className/style.

Data-attribute mapping

RadixBase UINote
[data-state="on" | "off"][data-pressed] (presence)Renamed. Off = absence of data-pressed.
[data-disabled][data-disabled]Same.

CSS var mapping

None on either side.


toggle-group

Part mapping: ToggleGroup.Root → ToggleGroup (callable single export), ToggleGroup.Item → Toggle (Base UI reuses the Toggle primitive as group items).

ToggleGroup.Root → ToggleGroup

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed.
type (required)"single" | "multiple" / —multiple (boolean, default false)Signature changed, same pattern as accordion.
valuestring (single) or string[] (multiple) / —valueSignature changed: always readonly Value[] (array), even single mode. value="bold" → value={["bold"]}.
defaultValuestring or string[] / —defaultValueSame array caveat.
onValueChange(value: string) => void or (value: string[]) => void / —onValueChangeSignature changed: (groupValue: Value[], eventDetails: ToggleGroup.ChangeEventDetails) => void. Always an array; single mode with nothing pressed = [] (Radix single mode signals this as "").
disabledboolean / falsedisabled (default false)Same.
rovingFocusboolean / true— droppedRoving focus is always on in Base UI; no opt-out. If you relied on rovingFocus={false} (every item tabbable), there is no direct workaround.
orientation"horizontal" | "vertical" / undefinedorientation (default 'horizontal')Same name; Base UI has an explicit default.
dir"ltr" | "rtl" / —— droppedUse DOM dir / DirectionProvider.
loopboolean / trueloopFocus (default true)Renamed.

ToggleGroup.Item → Toggle

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender (+ nativeButton, default true)Signature changed.
value (required)string / —valueSame meaning; on Base UI's Toggle it is optional in the type but required in practice for group membership.
disabledboolean / —disabled (default false)Same.

Note: the item also gains the full standalone Toggle API (pressed, defaultPressed, onPressedChange with eventDetails) since it IS the Toggle primitive; inside a group the group value normally drives pressed state.

Base UI only props worth knowing

  • multiple (covered above) and the always-array value model.
  • Items are plain Toggles, so per-item onPressedChange is available.
  • State-function className/style.

Data-attribute mapping

RadixBase UINote
Item [data-state="on" | "off"]Toggle [data-pressed] (presence)Renamed.
Item [data-disabled]Toggle [data-disabled]Same.
Root/Item [data-orientation]ToggleGroup [data-orientation]On the group only; items (Toggles) do not emit it.
—ToggleGroup [data-disabled], [data-multiple]Base UI only.

CSS var mapping

None on either side.


toolbar

Part mapping: Root → Root, Button → Button, Link → Link, Separator → Separator. Toolbar.ToggleGroup/Toolbar.ToggleItem are DROPPED as dedicated parts: compose the standalone ToggleGroup with <Toolbar.Button render={<Toggle />} value="..."> as items (Base UI docs pattern). Base UI adds Group and Input parts with no Radix equivalent.

Toolbar.Root → Toolbar.Root

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed.
orientation"horizontal" | "vertical" / "horizontal"orientation (default 'horizontal')Same.
dir"ltr" | "rtl" / —— droppedUse DOM dir / DirectionProvider.
loopboolean / trueloopFocus (default true)Renamed.

Toolbar.Button → Toolbar.Button

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserender (+ nativeButton, default true)Signature changed.
Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed. Both render <a>.

Toolbar.ToggleGroup → ToggleGroup (standalone, composed)

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed.
type (required)"single" | "multiple" / —multiple (boolean, default false)Moved: use the standalone ToggleGroup component inside Toolbar.Root; same mapping as toggle-group above.
value / defaultValuestring or string[] / —value / defaultValue on ToggleGroupAlways an array (see toggle-group).
onValueChange(value: string | string[]) => void / —onValueChange on ToggleGroup(groupValue: Value[], eventDetails) => void.
disabledboolean / falsedisabled on ToggleGroupSame.

Toolbar.ToggleItem → Toolbar.Button render={}

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / false—Moved: the composition IS the render prop: <Toolbar.Button render={<Toggle />} value="bold" />. Toolbar.Button supplies toolbar focus behavior, Toggle supplies pressed state.
value (required)string / —value (on the composed element)Same.
disabledboolean / —disabled (on Toolbar.Button, default false)Same; note focusableWhenDisabled defaults to true (disabled items stay focusable, Radix disabled items are not).

Toolbar.Separator → Toolbar.Separator

Radix propType / defaultBase UI equivalentMigration note
asChildboolean / falserenderSignature changed.
——orientationBase UI only: defaults to the OPPOSITE of the toolbar's orientation (horizontal toolbar → vertical separator), which matches Radix's automatic behavior; usually omit.

Base UI only props worth knowing

  • Root.disabled: disables the entire toolbar (no Radix equivalent).
  • Toolbar.Group (new part): groups related items, with a group-level disabled (default false).
  • Toolbar.Input (new part): <input> wired into toolbar arrow-key navigation; defaultValue, disabled (default false), focusableWhenDisabled (default true).
  • Button.disabled (default false) + Button.focusableWhenDisabled (default true): disabled buttons remain focusable for discoverability; set focusableWhenDisabled={false} for Radix-like behavior.
  • Button.nativeButton (default true), state-function className/style on all parts.

Data-attribute mapping

RadixBase UINote
[data-orientation] (Root, Button, ToggleGroup, ToggleItem, Separator)[data-orientation] (Root, Button, Link, Input, Group, Separator)Same; Separator's value is perpendicular to the toolbar.
ToggleItem [data-state="on" | "off"][data-pressed] (presence, from the composed Toggle)Renamed.
ToggleItem [data-disabled][data-disabled] (Root, Button, Input, Group)Same.
—Button/Input [data-focusable]Base UI only: present when focusable-while-disabled.

CSS var mapping

None on either side.