---
title: Upgrading to Base UI
description: Migrating consumer apps to the Base UI–backed registry (major release).
icon: circle-arrow-up
---

:::warning
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

```bash
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:

```bash
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](https://github.com/facebook/jscodeshift) codemod automates the mechanical, 1:1 changes and prints a
checklist of the context-dependent ones it leaves for you:

```bash
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`

```diff
- <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).

:::info
**Drawer** moved from vaul to Base UI's Drawer with the 1.9 upgrade, so it takes `render` too. Its other vaul-era
props (`direction`, `snapPoints`, `dismissible`) keep working; [Migrating from vaul](/docs/drawer#migrating-from-vaul) lists what changed.
:::

### `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.

```diff
- <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:

```diff
- <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.

:::info
`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

```diff
- 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:

```diff
- 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

```diff
- <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[]`:

```diff
- <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` |

:::info
`toggle`'s size was renamed back to `md` in a later release, when every component's base size was
aligned on `md`. The row above records the Base UI migration as it happened.
:::

## Dependency upgrades

- **chart** — `recharts` v2 → **v3**. recharts v3 removed component `defaultProps`, so any composition inside
  `ChartContainer` that relied on default axis/series props must now set them explicitly. v3 also pulls
  `@reduxjs/toolkit` / `react-redux` into the graph — a bundle delta worth measuring.
- **resizable** — `react-resizable-panels` v3 → **v4**. The `ResizablePanelGroup` `direction` prop is now `orientation`,
  and panel sizes are percentage strings (`defaultSize="50%"`, not `defaultSize={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](/docs/changelog).
