# Beste UI

> Beautiful, accessible blocks, pieces and components for shadcn/ui and Tailwind CSS, built for React and Next.js. Install one with the shadcn CLI and the code is yours to edit: no runtime dependency, nothing to upgrade.

Markdown rendition of https://ui.beste.co/blog/base-ui-the-new-default. Every page on the site has one: add `.md` to any address, or send `Accept: text/markdown`.

Last updated: 2026-07-04

Whole catalog for LLMs: https://ui.beste.co/llms.txt. Registry index: https://ui.beste.co/r/registry.json

---
[All posts](/blog)

July 4, 2026·15 min read

# Base UI Is the New shadcn/ui Default: A Complete Radix Migration Guide

shadcn/ui now ships on Base UI instead of Radix. This is the precise migration map: the asChild to render shift, the Portal-Positioner-Popup model, every renamed part, the call-site props that change, the behavior deltas to watch, and how Beste UI serves both libraries from one source.

base-uiradixshadcnreactmigration

![zieg](/_next/image?url=%2Fassets%2Fimages%2Fzieg.jpg&w=96&q=75&dpl=dpl_3WbX7mdvV35PNBYKTnk4fn25AFxE)

ziegbuilding beste.co

[zieg's website](https://zieg.beste.co)[zieg on X](https://x.com/forwardset)[zieg on LinkedIn](https://linkedin.com/in/ziegfiroyt)

On July 3, 2026, shadcn/ui changed the primitive library underneath every component it ships. New installs now come wired to Base UI, not Radix. If you run `npx shadcn@latest info`, the `base` field is the source of truth for which one a given project is on.

This is a bigger change than a version bump, and a smaller one than a rewrite. The component names you import stay the same. The wiring inside them, and a handful of props at the places you use them, do not. This post is the complete map: the four shifts that explain every difference, the full rename table, the call-site props that break, the behavior changes that compile fine but act differently, and a migration path that keeps the app building at every step.

First, the announcement, in the author's own words:

> Starting today, we are making [@base\_ui](https://x.com/base%5Fui) the default component library in shadcn/ui.  
>  
> First, a bit of history. When shadcn/ui launched in 2023, it was built on Radix. At the time, nothing else came close. Headless. Accessible. Composable.  
>  
> Fast forward a few years and the team who…
> 
> [shadcn (@shadcn) · July 3, 2026](https://x.com/shadcn/status/2073021571342520343)

### Why the default moved

Radix was the right foundation in 2023, and it still works today. What changed is not that Radix got worse. A second option earned the default through use rather than hype, and the shadcn team took the patient route to get there.

When Base UI reached beta, and again when it reached 1.0, the obvious question was whether it would replace Radix. Both times the answer was "not yet." Rather than force a swap, shadcn did the incremental thing: it added Base UI support to shadcn/create, recreated every component on it, rewrote the documentation for both libraries, and let people choose Radix or Base at setup. Then it watched what people actually shipped.

By the middle of 2026, every signal it had been waiting on had arrived:

* Base UI is stable. It sits at `@base-ui/react@1.6.0` with more than six million weekly downloads.
* It keeps getting better. The team ships new and genuinely useful primitives on a regular cadence, including ones Radix never had: Combobox, Autocomplete, Number Field, Checkbox Group, and object-valued Select among them.
* The maintainers use it themselves. Every new project they have started runs on Base UI.
* Users reach for it. New projects created through shadcn/create pick Base UI over Radix by roughly two to one.

So this was less a decision handed down than one the community had already made. Making it official flips three defaults: a fresh `shadcn init` now scaffolds on Base UI, shadcn/create shows Base UI as the default option, and the docs lead with Base UI while the Radix version stays one click away. Because the registry is where most teams get their components, the anatomy you copy into a new project is now Base UI by default.

None of that puts your current app at risk, and this is the part worth reading twice. Radix is not being deprecated. It stays supported, and every update and new component ships for both libraries, the only exception being a primitive that exists solely in Base UI. You are not required to migrate at all. Radix is mature, tested, and still runs in production for the shadcn team today, so if your app works, keep it. The rest of this post is for the moment you decide you want what Base UI adds. Almost every difference between the two libraries reduces to four repeating shifts, so that is where we start.

### The four shifts that explain everything

Learn these four and most of the diff reads itself.

#### 1\. Composition: `asChild` becomes `render`

Radix merged props onto a child through a boolean. Base UI takes the element directly through a `render` prop.

diffCopy

```
- <DialogTrigger asChild>
-   <Button>Open</Button>
- </DialogTrigger>
+ <DialogTrigger render={<Button />}>Open</DialogTrigger>
```

The one addition to remember: when `render` swaps in an element that is not a native `<button>`, tell Base UI so with `nativeButton={false}`.

tsxCopy

```
<Button render={<a href="/docs" />} nativeButton={false}>
  Read the docs
</Button>
```

This applies to every trigger and close part: `DialogTrigger`, `SheetTrigger`, `AlertDialogTrigger`, `DropdownMenuTrigger`, `PopoverTrigger`, `TooltipTrigger`, `CollapsibleTrigger`, `DialogClose`, `NavigationMenuLink`, `BreadcrumbLink`, and more. For hand-rolled polymorphic components that used the manual Slot idiom (`const Comp = asChild ? Slot.Root : "a"`), the replacement is `useRender` plus `mergeProps` from `@base-ui/react`. One caveat there: object literals that carry `data-*` keys must be cast (`as React.ComponentProps<"a">`) before they go into `mergeProps`, or the type checker rejects every one. The plain `button.tsx` wrapper is simpler than that: it migrates straight to the real `@base-ui/react/button` primitive, which accepts `render` natively.

#### 2\. Positioning: `Portal > Content` becomes `Portal > Positioner > Popup`

Every floating surface (popover, tooltip, menu, select, hover card) splits the old single `Content` into three roles. Positioning props move to a new `Positioner`; the styled box becomes `Popup`.

diffCopy

```
  <PopoverPortal>
-   <PopoverContent side="bottom" sideOffset={8} align="start">
-     ...
-   </PopoverContent>
+   <PopoverPositioner side="bottom" sideOffset={8} align="start">
+     <PopoverPopup>...</PopoverPopup>
+   </PopoverPositioner>
  </PopoverPortal>
```

For dialogs and sheets the mirror change is `Overlay` to `Backdrop`, and centered modals use `Popup` with no `Positioner` at all. There is one discipline this model demands, and no type error will catch it for you: when a wrapper exposes positioning props via `Pick<...Positioner.Props, "align" | "side" | ...>`, you must destructure each one and forward it to `<Positioner>` yourself. Miss it, and the props fall through `...props` onto the Popup, and positioning silently breaks.

Declare, destructure, forward

The Positioner rule is three steps, every time, for every overlay wrapper: declare the positioning props in the type, destructure them out of `props`, and pass them explicitly to `Positioner`. If you only do the first, they land on the wrong DOM node and the popup stops respecting `side` and `align` with no compiler complaint.

#### 3\. State hooks: value attributes become presence attributes

Radix encoded state in the value of a `data-state` attribute. Base UI uses presence attributes instead, which changes both your CSS selectors and your animation strategy.

| Radix                         | Base UI         |
| ----------------------------- | --------------- |
| data-\[state=open\]:          | data-open:      |
| data-\[state=closed\]:        | data-closed:    |
| data-\[state=checked\]:       | data-checked:   |
| data-\[state=active\]: (tabs) | data-active:    |
| data-\[state=on\]: (toggle)   | data-pressed:   |
| group-data-\[state=open\]     | group-data-open |

Animations move from keyframe utilities to transitions. The Radix idiom of `data-[state=open]:animate-in` and `data-[state=closed]:animate-out` becomes `data-starting-style:` and `data-ending-style:` on a real CSS transition. Do not translate the `animate-in` utilities one for one; restate the intent as a transition between a starting and ending style. CSS variables rename on the same principle: `--radix-popover-content-transform-origin` becomes `--transform-origin`, `--radix-popover-trigger-width` becomes `--anchor-width`, and the accordion and collapsible height vars become `--accordion-panel-height` and `--collapsible-panel-height`.

One subtlety that bites: when a part changes its rendered element (Checkbox, Switch, and Radio roots render a `<span>` in Base UI rather than a `<button>`), the Tailwind `disabled:` and `:disabled` variants become dead code, because a `<span>` has no disabled pseudo-class. Replace them with `data-disabled:` equivalents.

#### 4\. Callbacks: one signature, plus a reason

Radix shipped a callback per dismissal interaction. Base UI consolidates them. `onOpenChange` gains a second `eventDetails` argument, and the per-interaction handlers collapse into a `reason` you branch on.

diffCopy

```
- <DialogContent
-   onEscapeKeyDown={(e) => e.preventDefault()}
-   onPointerDownOutside={(e) => e.preventDefault()}
- >
+ <Dialog
+   onOpenChange={(open, details) => {
+     if (details.reason === "escape-key" || details.reason === "outside-press") {
+       details.cancel()
+     }
+   }}
+ >
```

`eventDetails.cancel()` is the new `event.preventDefault()`. The reasons are a small union per component (`'escape-key'`, `'outside-press'`, `'focus-out'`, and friends). Passing an old single-argument handler stays type safe, so only the handlers that actually read the event need attention.

### The rename map

Beyond the four shifts, a set of parts simply have new names. This is the reference you keep open while migrating.

| Radix part                                 | Base UI part                                          |
| ------------------------------------------ | ----------------------------------------------------- |
| Overlay                                    | Backdrop                                              |
| Content (overlay components)               | Popup (inside Positioner)                             |
| Content (accordion, collapsible, tabs)     | Panel                                                 |
| tabs Trigger                               | Tab                                                   |
| menu Label                                 | GroupLabel                                            |
| menu ItemIndicator                         | CheckboxItemIndicator or RadioItemIndicator           |
| Sub and SubTrigger                         | SubmenuRoot and SubmenuTrigger                        |
| slider Range                               | Indicator (plus a new Control)                        |
| select Viewport                            | List                                                  |
| select ScrollUpButton and ScrollDownButton | ScrollUpArrow and ScrollDownArrow                     |
| scroll-area Scrollbar and Thumb            | Scrollbar and Thumb (renamed exports)                 |
| nav-menu Indicator                         | Icon                                                  |
| hover-card HoverCard\*                     | PreviewCard\* (public wrapper names stay HoverCard\*) |
| radio-group Item and Indicator             | Radio.Root and Radio.Indicator                        |
| DropdownMenu (the whole primitive)         | Menu                                                  |
| Label primitive                            | native <label>                                        |

A few primitives have no Base UI counterpart, and the migration is to plain platform features rather than a new part. AspectRatio becomes a div with the CSS `aspect-ratio` property. Label becomes a native `<label>` (or `Field.Label` inside a form). VisuallyHidden becomes the `sr-only` class. Direction is provided by `DirectionProvider` with a `direction` prop instead of `dir`.

### The call-site props that change

The rename map covers component internals. This section is the part that reaches into your application code, because these props change where you _use_ the components. Sweep for them across everything outside `components/ui`.

| Component              | What changes at the call site                                                                                                                                    |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Accordion              | type="single" and collapsible are dropped; value and defaultValue are always arrays; multiple-open is a multiple boolean.                                        |
| Tabs                   | activationMode is dropped; Base UI defaults to manual activation. Opt back into automatic with <TabsList activateOnFocus>.                                       |
| Select                 | position becomes alignItemWithTrigger (a boolean); onValueChange widens to (value \| null, details); a placeholder is a { value: null } entry in an items array. |
| ToggleGroup            | type becomes a multiple boolean; value is always an array.                                                                                                       |
| Slider                 | a single thumb accepts a plain number (no array wrapper); onValueCommit becomes onValueCommitted; inverted is removed.                                           |
| Checkbox               | checked="indeterminate" splits into a separate indeterminate boolean plus a boolean checked.                                                                     |
| Tooltip                | delayDuration becomes delay; skipDelayDuration becomes timeout; disableHoverableContent has no equivalent.                                                       |
| Dialog and AlertDialog | onOpenAutoFocus becomes initialFocus, onCloseAutoFocus becomes finalFocus, both element based rather than event based.                                           |
| Avatar                 | Image.delayMs becomes delay.                                                                                                                                     |
| Separator              | decorative is dropped (the separator is always semantic).                                                                                                        |
| NavigationMenu         | delayDuration becomes delay (default 200 becomes 50); viewport is gone.                                                                                          |
| Popover and HoverCard  | openDelay and closeDelay move from Root onto the Trigger.                                                                                                        |

The single most common cause of a broken build after the wrappers compile is the controlled Select. Radix gave you a `string`; Base UI gives you `Value | null`.

diffCopy

```
- const [value, setValue] = useState("")
+ const [value, setValue] = useState<string | null>(null)

  <Select value={value} onValueChange={setValue}>
```

The array-shape family

Accordion, ToggleGroup, and single-mode Tabs share one mental model in Base UI: the value is always an array, even for single selection. `value="a"` becomes `value={["a"]}`, and a controlled single value round-trips as `value={[v]}` with `onValueChange={(next) => setV(next[0] ?? "")}`. Get this one habit and three components stop surprising you.

### A worked example: the Select

Select changes the most, so it is the best teacher. In Radix, one `Content` did positioning, collision handling, and the panel. In Base UI, those split, the viewport becomes a `List`, and the root can take an `items` prop so the trigger can render a label instead of a raw value.

diffCopy

```
- <Select value={fruit} onValueChange={setFruit}>
-   <SelectTrigger>
-     <SelectValue placeholder="Select a fruit" />
-   </SelectTrigger>
-   <SelectContent position="popper">
-     <SelectItem value="apple">Apple</SelectItem>
-     <SelectItem value="banana">Banana</SelectItem>
-   </SelectContent>
- </Select>
+ <Select items={items} value={fruit} onValueChange={setFruit}>
+   <SelectTrigger>
+     <SelectValue />
+   </SelectTrigger>
+   <SelectContent alignItemWithTrigger={false}>
+     {items.map((item) => (
+       <SelectItem key={item.value} value={item.value}>
+         {item.label}
+       </SelectItem>
+     ))}
+   </SelectContent>
+ </Select>
```

The placeholder is now the `{ label: "Select a fruit", value: null }` entry at the top of `items`, and `fruit` is typed `string | null`. In return you get things Radix never offered: `multiple`, object values via `itemToStringValue`, and a render-function `SelectValue` for custom trigger content.

### A worked example: the dropdown menu

Menus carry the most renamed parts, but they follow the four shifts exactly. `Label` becomes `GroupLabel`, `ItemIndicator` splits into `CheckboxItemIndicator` and `RadioItemIndicator`, `Sub` becomes `SubmenuRoot`, and `Content` expands into `Portal > Positioner > Popup`. The item selection model also changes: Radix used `onSelect` with `event.preventDefault()` to keep the menu open, while Base UI uses `onClick` plus a `closeOnClick` boolean. Typeahead text moves from `textValue` to `label`.

Checkbox and radio items no longer close on click

This is the behavior delta that surprises people. In Base UI, `closeOnClick` defaults to `false` on `CheckboxItem` and `RadioItem`, so toggling one keeps the menu open. Radix closed it by default. If you relied on the close, set `closeOnClick` explicitly. It compiles either way, so nothing warns you.

### Behavior deltas that compile but act differently

These are the changes that pass the type checker and pass the build, then feel wrong in the browser. Flag them, verify them by hand, and decide each one on purpose.

* Tabs default to manual activation: arrow keys move focus without switching the panel. Add `activateOnFocus` to the list to restore automatic switching.
* Menu checkbox and radio items keep the menu open on click (`closeOnClick` defaults to false).
* NavigationMenu hover delay is 50ms, down from Radix's 200ms.
* Tooltips are always hoverable; there is no `disableHoverableContent`.
* ScrollArea loses its `type` prop; scrollbar visibility is now pure CSS driven by `data-hovering` and `data-scrolling`.
* Accordion single mode is always collapsible; to forbid closing the last open item, control `value` and cancel updates that would empty the array.

### Migrating without a big-bang rewrite

The safe path is the strangler fig, one component at a time, with both libraries installed side by side. `@base-ui/react` and the Radix packages coexist without conflict, so you remove Radix only after the last component moves.

1. Baseline first. Run your typecheck and build before you touch dependencies, so pre-existing failures are never blamed on the migration.
2. Install `@base-ui/react` alongside Radix. Work on a branch, one commit per component.
3. Migrate leaf and shared wrappers first (button and label before the components that import them). If a component still imports another wrapper that is on Radix, migrate that one first, bottom up.
4. Write the migrated version to `component-base.tsx`, leaving the original untouched. Repoint consumers one at a time, typechecking after each. When nothing imports the original, delete it, rename `-base` to the real name, and commit.
5. Sweep application code for the call-site props above. The break surface there is larger than `asChild` alone.

For a shadcn project with a known style, the CLI does the heavy lifting: the base variant of each component is served at `https://ui.shadcn.com/r/styles/base-<style>/<component>.json`, so you can fetch the exact base anatomy with your project's icon and font resolution rather than reconstructing it by hand. One rule holds no matter how clean a merge looks: grep every migrated file for `radix-ui`, `@radix-ui`, and any leftover placeholder before you call it done. A clean three-way merge is not proof of a clean file.

#### The official migration skill

You do not have to drive these transforms by hand. shadcn shipped tooling for this exact workflow, a skill your coding agent runs component by component. In their words:

> When you're ready to migrate, we built a skill for you.
> 
> `npx skills add shadcn/ui`
> 
> Then ask your coding agent: "migrate alert-dialog to base ui"
> 
> It's progressive: migrate one component at a time while your project stays green. Stop halfway, ship, come back next week. It picks up where you left off.

\- shadcn

![The shadcn/ui skill migrating a component from Radix to Base UI](https://oud.pics/blog/HMTWRffWQAAt87l.png)

That is the same strangler-fig model described above, automated: the project stays green at every step, and the work is resumable, so a large migration becomes a series of small, shippable commits instead of one risky rewrite.

What migration never touches

Several shadcn components are not Radix at all, and they stay exactly as they are: cmdk (command), vaul (drawer), sonner, input-otp, react-day-picker (calendar), and recharts (chart). If a migration tool offers to rewrite them, it is wrong. Leave them, and note them as intentionally untouched.

### How Beste UI serves both, from one source

We took a deliberate stance so you never have to choose a camp at install time. Our canonical component source stays on Radix, and the Base UI variant is generated at build time by a codemod that applies exactly the transforms in this post: `asChild` to `render` with `nativeButton`, the array-shape value rewrites, the data-attribute and CSS-variable renames, the callback reshaping. The Base UI output is served from a parallel set of URLs under `/r-base/`, and the install control defaults to Base UI to match the new shadcn default.

The practical result is that every block, piece, and component here installs in whichever flavor your project speaks, from the same source of truth:

bashCopy

```
# Base UI (the default)
npx shadcn@latest add https://ui.beste.co/r-base/hero7

# Radix, still first class
npx shadcn@latest add https://ui.beste.co/r/hero7
```

The block below is that idea in one frame. It is a real component from the registry, and it is the same source whether you install it as Base UI or as Radix.

Centered Hero with Media

[View block](/block/hero7)

Loading preview…

hero7 from the registry. One source, generated into either Base UI or Radix at build time.

### When to migrate, and when to wait

New project: take the default and start on Base UI. You get the newer primitives for free, and you are aligned with where the registry is heading.

Existing Radix app that is happy: there is no forced march. Radix keeps working, and here it keeps its own `/r/` URLs that never change. Migrate when you have a reason: you want a primitive Base UI adds and Radix lacks (Combobox, Autocomplete, Number Field, Checkbox Group, object-valued or multiple Select), or you want to be on the same anatomy as everything you install next. When you do migrate, do it leaf first, one component per commit, and let the four shifts carry most of the work.

The names stayed. The wiring changed. Now you have the map for both.

## Build it with real blocks

Every section in this post is a component you can copy into your own codebase and own outright.

[Browse blocksBrowse blocks](/blocks)[Read more postsRead more posts](/blog)

## Ship the interface, keep the code.

New blocks, pieces and components every week. Install one with a command and it is yours to edit. No runtime dependency, no upgrade path to fight.

[Browse the libraryBrowse the library](/blocks)[See pricingSee pricing](/pricing)

### Library

* [Blocks](/blocks)
* [Pages](/pages)
* [Pieces](/pieces)
* [Components](/components)
* [Search](/search)

### Learn

* [Docs](/docs)
* [AI & MCP](/docs/mcp)
* [Blog](/blog)
* [Free tools](/tools)
* [What's new?](/changelog)
* [Website Builder](https://beste.co)

### More

* [Pricing](/pricing)
* [Referrals](/referrals)
* [License](/license)
* [GitHub](https://github.com/beste-co/beste-ui)

### Shadcn Blocks

* [Shadcn Hero Blocks](/blocks/hero)
* [Shadcn Feature Blocks](/blocks/feature)
* [Shadcn Pricing Blocks](/blocks/pricing)
* [Shadcn CTA Blocks](/blocks/cta)
* [Shadcn FAQ Blocks](/blocks/faq)
* [Shadcn About Blocks](/blocks/about)
* [Shadcn Stats Blocks](/blocks/stats)
* [Shadcn Footer Blocks](/blocks/footer)
* [Shadcn Navigation Blocks](/blocks/navigation)
* [Shadcn Auth Blocks](/blocks/auth)
* [Shadcn Ecommerce Blocks](/blocks/ecommerce)
* [Shadcn Portfolio Blocks](/blocks/portfolio)
* [Shadcn Showcase Blocks](/blocks/showcase)
* [Shadcn Careers Blocks](/blocks/careers)
* [Shadcn Onboarding Blocks](/blocks/onboarding)
* [Shadcn Coming Soon Blocks](/blocks/coming-soon)
* [Shadcn Post Blocks](/blocks/post)
* [Shadcn Legal Blocks](/blocks/legal)
* [Shadcn Workflow Blocks](/blocks/workflow)
* [Shadcn News Blocks](/blocks/news)

© 2026, [Beste](https://beste.co). All rights reserved.