Upgrading to Base UI
Migrating consumer apps to the Base UI–backed registry (major release).
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.9.0
bun remove radix-ui # only if nothing else uses Radix directly
npm / 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 rest
3. 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 ./src
Add -d -p for a dry run that prints the diff without writing.
| Auto-applied | Flagged for manual review |
|---|---|
asChild → render |
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; 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, Drawer, Credenza) and the polymorphic parts (Badge, Item, Breadcrumb, ButtonGroupText, Button).
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.
data-[state=…] → native Base UI data attributes
- data-[state=open]:bg-accent
+ data-[popup-open]:bg-accent
Remap 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, Drawer included, replaced Radix’s onOpenAutoFocus with Base UI’s initialFocus.
initialFocus={false} replaces onOpenAutoFocus={(e) => e.preventDefault()}.
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.