Skill 01 · Migrate Radix To Base
Subchapter 1.3
disclosure.mdMarkdown20 KBView on GitHub
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.className and style also accept a (state) => value function form.data-[state=...] value attributes become Base UI presence attributes (data-open, data-closed, data-pressed, data-active).eventDetails argument ({ reason, event, cancel(), ... }).dir props are dropped everywhere; Base UI reads direction from the DOM dir attribute / DirectionProvider.Part mapping: Root → Root, Item → Item, Header → Header, Trigger → Trigger, Content → Panel.
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature changed. Pass element to render instead of wrapping a child. |
type (required) | "single" | "multiple" / — | multiple | Signature changed. type="multiple" → multiple (boolean, default false); type="single" → omit. |
value | string (single) or string[] (multiple) / — | value | Signature changed. Base UI is ALWAYS an array (Value[], Value = any), even in single mode: value="a" → value={["a"]}. |
defaultValue | string or string[] / — | defaultValue | Same array caveat as value. |
onValueChange | (value: string) => void or (value: string[]) => void / — | onValueChange | Signature changed: (value: Value[], eventDetails: Accordion.Root.ChangeEventDetails) => void. Always receives an array; unwrap value[0] for single mode. |
collapsible | boolean / false | — dropped | Base 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. |
disabled | boolean / false | disabled (default false) | Same. |
dir | "ltr" | "rtl" / "ltr" | — dropped | Use 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. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature changed. |
value (required) | string / — | value | Renamed constraint: Base UI value is any and OPTIONAL (auto-generated from index when omitted). Keep passing strings for parity. |
disabled | boolean / false | disabled (default false) | Same. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature changed. Both render <h3> by default. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render (+ nativeButton, default true) | Signature changed. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature changed. |
forceMount | true | undefined / — | keepMounted (boolean, default false) | Renamed. forceMount → keepMounted (closed panel stays in DOM, hidden). Also available on Root to apply to all panels. |
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.| Radix | Base UI | Note |
|---|---|---|
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-orientation | Deprecated 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). |
| Radix | Base UI |
|---|---|
--radix-accordion-content-height | --accordion-panel-height |
--radix-accordion-content-width | --accordion-panel-width |
Part mapping: Root → Root, Trigger → Trigger, Content → Panel.
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature changed. |
defaultOpen | boolean / — | defaultOpen (default false) | Same. |
open | boolean / — | open | Same. |
onOpenChange | (open: boolean) => void / — | onOpenChange | Signature changed: (open: boolean, eventDetails: Collapsible.Root.ChangeEventDetails) => void. |
disabled | boolean / — | disabled (default false) | Same. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render (+ nativeButton, default true) | Signature changed. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature changed. |
forceMount | true | undefined / — | keepMounted (boolean, default false) | Renamed. |
Panel.hiddenUntilFound (default false): find-in-page support via hidden="until-found"; overrides keepMounted.Trigger.nativeButton (default true).className / style state-function forms.| Radix | Base UI | Note |
|---|---|---|
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. |
| Radix | Base UI |
|---|---|
--radix-collapsible-content-height | --collapsible-panel-height |
--radix-collapsible-content-width | --collapsible-panel-width |
Part mapping: Root → Root, List → List, Trigger → Tab, Content → Panel. Base UI adds an Indicator part with no Radix equivalent.
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature changed. |
defaultValue | string / — | defaultValue | Signature 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. |
value | string / — | value | Same shape for string values; type widened to any. |
onValueChange | (value: string) => void / — | onValueChange | Signature changed: (value: Tabs.Tab.Value, eventDetails: Tabs.Root.ChangeEventDetails) => void. |
orientation | "horizontal" | "vertical" / "horizontal" | orientation (default 'horizontal') | Same. |
dir | "ltr" | "rtl" / — | — dropped | Use 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. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature changed. |
loop | boolean / true | loopFocus (default true) | Renamed. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render (+ nativeButton, default true) | Signature changed. |
value (required) | string / — | value (required) | Type widened to Tabs.Tab.Value (any); strings unchanged. |
disabled | boolean / false | disabled | Same. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature changed. |
value (required) | string / — | value (required) | Type widened; strings unchanged. |
forceMount | true | undefined / — | keepMounted (boolean, default false) | Renamed. Hidden panels stay in DOM with data-hidden. |
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.| Radix | Base UI | Note |
|---|---|---|
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. |
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.
Part mapping: Toggle.Root → Toggle (single-part; Base UI export is directly callable, no .Root).
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render (+ nativeButton, default true) | Signature changed. |
defaultPressed | boolean / — | defaultPressed (default false) | Same. |
pressed | boolean / — | pressed | Same. |
onPressedChange | (pressed: boolean) => void / — | onPressedChange | Signature changed: (pressed: boolean, eventDetails: Toggle.ChangeEventDetails) => void. |
disabled | boolean / — | disabled (default false) | Same. |
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.| Radix | Base UI | Note |
|---|---|---|
[data-state="on" | "off"] | [data-pressed] (presence) | Renamed. Off = absence of data-pressed. |
[data-disabled] | [data-disabled] | Same. |
None on either side.
Part mapping: ToggleGroup.Root → ToggleGroup (callable single export), ToggleGroup.Item → Toggle (Base UI reuses the Toggle primitive as group items).
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature changed. |
type (required) | "single" | "multiple" / — | multiple (boolean, default false) | Signature changed, same pattern as accordion. |
value | string (single) or string[] (multiple) / — | value | Signature changed: always readonly Value[] (array), even single mode. value="bold" → value={["bold"]}. |
defaultValue | string or string[] / — | defaultValue | Same array caveat. |
onValueChange | (value: string) => void or (value: string[]) => void / — | onValueChange | Signature changed: (groupValue: Value[], eventDetails: ToggleGroup.ChangeEventDetails) => void. Always an array; single mode with nothing pressed = [] (Radix single mode signals this as ""). |
disabled | boolean / false | disabled (default false) | Same. |
rovingFocus | boolean / true | — dropped | Roving 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" / undefined | orientation (default 'horizontal') | Same name; Base UI has an explicit default. |
dir | "ltr" | "rtl" / — | — dropped | Use DOM dir / DirectionProvider. |
loop | boolean / true | loopFocus (default true) | Renamed. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render (+ nativeButton, default true) | Signature changed. |
value (required) | string / — | value | Same meaning; on Base UI’s Toggle it is optional in the type but required in practice for group membership. |
disabled | boolean / — | 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.
multiple (covered above) and the always-array value model.Toggles, so per-item onPressedChange is available.className/style.| Radix | Base UI | Note |
|---|---|---|
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. |
None on either side.
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.
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature changed. |
orientation | "horizontal" | "vertical" / "horizontal" | orientation (default 'horizontal') | Same. |
dir | "ltr" | "rtl" / — | — dropped | Use DOM dir / DirectionProvider. |
loop | boolean / true | loopFocus (default true) | Renamed. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render (+ nativeButton, default true) | Signature changed. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature changed. Both render <a>. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature 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 / defaultValue | string or string[] / — | value / defaultValue on ToggleGroup | Always an array (see toggle-group). |
onValueChange | (value: string | string[]) => void / — | onValueChange on ToggleGroup | (groupValue: Value[], eventDetails) => void. |
disabled | boolean / false | disabled on ToggleGroup | Same. |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / 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. |
disabled | boolean / — | disabled (on Toolbar.Button, default false) | Same; note focusableWhenDisabled defaults to true (disabled items stay focusable, Radix disabled items are not). |
| Radix prop | Type / default | Base UI equivalent | Migration note |
|---|---|---|---|
asChild | boolean / false | render | Signature changed. |
| — | — | orientation | Base UI only: defaults to the OPPOSITE of the toolbar’s orientation (horizontal toolbar → vertical separator), which matches Radix’s automatic behavior; usually omit. |
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.| Radix | Base UI | Note |
|---|---|---|
[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. |
None on either side.
This file