Upgrading to Base UI
Migrating consumer apps to the Base UI–backed registry (major release).
This is a major release. The registry's underlying primitive moved from radix-ui to @base-ui/react, and the
Radix-compatibility shim was removed. Unlike the earlier previews (which preserved the old API via a shim), the
component API now changes — apps that vendor these components must update the dependency and a handful of
call-site patterns.
Who this is for
Any project that installs components via shadcn add "https://redpanda-ui-registry.netlify.app/r/<component>" (or the
@redpanda/... namespaced form). If you only use the docs site or the playground, no action is needed.
1. Install the new peer dependency
bun add @base-ui/react@^1.4.0
bun remove radix-ui # only if nothing else uses Radix directlynpm / pnpm / yarn work the same way.
2. Re-run shadcn add
Re-install every component you consume so the latest Base UI–backed source is copied in:
bunx shadcn@latest add @redpanda/button @redpanda/dialog @redpanda/popover # …and the rest3. Update call sites
The compat shim is gone, so these Radix-era patterns must change in your own code.
Run the codemod (recommended)
A jscodeshift codemod automates the mechanical, 1:1 changes and prints a checklist of the context-dependent ones it leaves for you:
bunx jscodeshift -t https://redpanda-ui-registry.netlify.app/codemods/base-ui-migration.ts \
--parser=tsx --extensions=tsx,ts ./srcAdd -d -p for a dry run that prints the diff without writing.
| Auto-applied | Flagged for manual review |
|---|---|
asChild → render (skips the vaul Drawer) | data-[state=open|closed] and any other residual data-[state=…] — context-dependent (data-open, data-popup-open, data-panel-open) |
data-[state=checked|unchecked|on|active] → data-[checked|unchecked|pressed|active] | onOpenAutoFocus → initialFocus (Drawer is exempt); Accordion type/collapsible and ResizablePanelGroup direction |
onSelect → onClick on menu items (adds closeOnClick={false} when the handler called preventDefault) | the now-redundant event.preventDefault() left inside a converted handler |
The remaining sections describe each change so you can resolve the flagged items.
asChild → render
- <DialogTrigger asChild><Button>Open</Button></DialogTrigger>
+ <DialogTrigger render={<Button>Open</Button>} />Affects every trigger / anchor / close (Dialog, AlertDialog, Popover, HoverCard, DropdownMenu, ContextMenu, Menubar, Collapsible, Tooltip) and the polymorphic parts (Badge, Item, Breadcrumb, ButtonGroupText, Button).
Exception: the vaul-based Drawer keeps asChild. The responsive Credenza wrapper now takes
render like the rest of the library (it bridges to the vaul Drawer internally on mobile).
onSelect → onClick (menu items)
Base UI's menu items fire onClick, not Radix's onSelect. Because onSelect is a valid DOM attribute it still
type-checks, so this fails silently — the handler simply never runs.
- <DropdownMenuItem onSelect={() => onEdit(id)}>Edit</DropdownMenuItem>
+ <DropdownMenuItem onClick={() => onEdit(id)}>Edit</DropdownMenuItem>When you used event.preventDefault() to keep the menu open (e.g. opening a dialog from a menu item), express that
with closeOnClick={false} instead:
- <DropdownMenuItem onSelect={(e) => { e.preventDefault(); setOpen(true); }}>Delete</DropdownMenuItem>
+ <DropdownMenuItem closeOnClick={false} onClick={() => setOpen(true)}>Delete</DropdownMenuItem>Affects DropdownMenu, ContextMenu, and Menubar Item / CheckboxItem / RadioItem. The registry now types
onSelect as never on these, so passing it is a compile error rather than a silent no-op.
onSelect stays valid on non-menu components that genuinely use it — Command items, Tags, and the Calendar
(react-day-picker). The codemod is scoped by tag name and leaves those untouched.
data-[state=…] → native Base UI data attributes
- data-[state=open]:bg-accent
+ data-[popup-open]:bg-accentRemap your Tailwind selectors:
| Radix (old) | Base UI (new) |
|---|---|
data-[state=open] / data-[state=closed] | data-open / data-closed (menu & popover triggers use data-popup-open) |
data-[state=checked] / data-[state=unchecked] | data-checked / data-unchecked |
toggle / toggle-group data-[state=on] | data-pressed |
accordion data-[state=open] | data-panel-open |
| disabled state | data-disabled |
Callback signatures
Base UI passes event details as a second argument:
- onValueChange={(value) => …}
+ onValueChange={(value, eventDetails) => …}Applies to onValueChange / onOpenChange / onCheckedChange / onPressedChange.
onOpenAutoFocus → initialFocus
The overlay primitives replaced Radix's onOpenAutoFocus with Base UI's initialFocus. (The vaul Drawer keeps
onOpenAutoFocus.)
Accordion
- <Accordion type="single" collapsible> {/* single + collapsible is now the default */}
+ <Accordion>
- <Accordion type="multiple" value={["item-1"]}> {/* array-valued */}
+ <Accordion multiple value={["item-1"]}>ToggleGroup
Like Accordion, ToggleGroup dropped Radix's type discriminator. value/defaultValue are now always string[], even for single-select, and onValueChange receives a string[]:
- <ToggleGroup type="single" value="left"> {/* single is the default */}
+ <ToggleGroup value={["left"]}>
- <ToggleGroup type="multiple" value={["bold","italic"]}>
+ <ToggleGroup multiple value={["bold","italic"]}>Single-select also moved from role="radiogroup"/role="radio" to native toggle semantics (aria-pressed).
Variant / size renames
These props were renamed to match shadcn — TypeScript flags the size changes, but variant string literals fail silently (the component falls back to its default), so audit these by hand:
| Component | Old | New |
|---|---|---|
toggle | base variant="ghost" | variant="default" |
toggle | size="md" | size="default" |
alert-dialog | AlertDialogCancel default ghost | outline |
Dependency upgrades
- chart —
rechartsv2 → v3. recharts v3 removed componentdefaultProps, so any composition insideChartContainerthat relied on default axis/series props must now set them explicitly. v3 also pulls@reduxjs/toolkit/react-reduxinto the graph — a bundle delta worth measuring. - resizable —
react-resizable-panelsv3 → v4. TheResizablePanelGroupdirectionprop is noworientation, and panel sizes are percentage strings (defaultSize="50%", notdefaultSize={50}).
Reference
The breaking changes for this release are documented above and in the release changeset
(.changeset/base-ui-shadcn-realignment.md). Subsequent per-component changes are tracked in the
changelog.