Migrating to Base UI
Learn how to migrate your components from Radix UI to Base UI.
What's new?
- All components are now built on Base UI (
@base-ui/react) instead of Radix UI, following shadcn/ui making Base UI its default library. - The
asChildprop is gone. Every component that supported it now accepts Base UI'srenderprop. - The drawer component now uses Base UI's drawer instead of
vaul. - Popup components (dropdown menu, popover, tooltip, select...) accept
side,sideOffset,alignandalignOffsetdirectly on their content component. - State is exposed through Base UI's data attributes (
data-open,data-checked,data-active...) instead of Radix'sdata-state.
Visit changelog to see all the changes.
Old docs
If you want to see the old docs, they are hosted on old.neobrutalism.dev.
1. Install Base UI
pnpm add @base-ui/reactIf you are starting a new project, initialize shadcn with Base UI as the component library:
pnpm dlx shadcn@latest init -b base2. Set up portals
Base UI renders popups (dialogs, menus, tooltips...) through portals. To keep them above the rest of your page, add isolate to the element that wraps your application and make body a positioned element, as recommended in the Base UI quick start:
<body className="relative isolate">{children}</body>3. Re-install the components
Install the components you use like you would usually do, either with the shadcn CLI command from each component page or by copying the component manually. The new files replace the old ones.
4. Remove the old packages
Once no component imports Radix anymore, remove the Radix packages together with vaul (replaced by Base UI's drawer), sonner (the toast component is gone) and next-themes (dark mode is gone):
pnpm remove @radix-ui/react-accordion @radix-ui/react-alert-dialog @radix-ui/react-avatar @radix-ui/react-checkbox @radix-ui/react-collapsible @radix-ui/react-context-menu @radix-ui/react-dialog @radix-ui/react-dropdown-menu @radix-ui/react-hover-card @radix-ui/react-label @radix-ui/react-menubar @radix-ui/react-navigation-menu @radix-ui/react-popover @radix-ui/react-progress @radix-ui/react-radio-group @radix-ui/react-scroll-area @radix-ui/react-select @radix-ui/react-slider @radix-ui/react-slot @radix-ui/react-switch @radix-ui/react-tabs @radix-ui/react-toast @radix-ui/react-tooltip vaul sonner next-themes5. Update your code
asChild becomes render
Instead of wrapping your element with asChild, pass the element to the render prop. The children stay on the Base UI component.
// Before
<DialogTrigger asChild>
<Button variant="neutral">Open</Button>
</DialogTrigger>
// After
<DialogTrigger render={<Button variant="neutral" />}>Open</DialogTrigger>This applies to every trigger and close component, Badge, BreadcrumbLink, Collapsible, NavigationMenuLink and the sidebar components (SidebarMenuButton, SidebarMenuSubButton, SidebarMenuAction, SidebarGroupLabel, SidebarGroupAction).
// Before
<SidebarMenuButton asChild>
<a href="/home">Home</a>
</SidebarMenuButton>
// After
<SidebarMenuButton render={<a href="/home" />}>Home</SidebarMenuButton>Button as a link
Button is now built on Base UI's button, which always sets role="button". Do not render it as a link with render={<a />}. Use the buttonVariants helper on a regular anchor or Next.js link instead:
import Link from "next/link"
import { buttonVariants } from "@/components/ui/button"
;<Link href="/docs" className={buttonVariants({ variant: "neutral" })}>
Docs
</Link>Accordion
type="single" and collapsible are gone. Items are always collapsible, one item is open at a time by default and the multiple prop allows several open items. value and defaultValue are now arrays.
// Before
<Accordion type="single" collapsible defaultValue="item-1">
// After
<Accordion defaultValue={["item-1"]}>
<Accordion multiple defaultValue={["item-1", "item-2"]}>Select
SelectValue renders the raw value unless the select knows the labels, so pass an items prop to Select (an object or an array of { value, label }). position="popper" is removed. Use alignItemWithTrigger on SelectContent if you want the native-like behavior where the selected item overlaps the trigger.
const fruits = {
apple: "Apple",
banana: "Banana",
}
;<Select items={fruits}>
<SelectTrigger>
<SelectValue placeholder="Select a fruit" />
</SelectTrigger>
<SelectContent>
<SelectItem value="apple">Apple</SelectItem>
<SelectItem value="banana">Banana</SelectItem>
</SelectContent>
</Select>Drawer
The drawer is built on Base UI's drawer. direction is now swipeDirection, shouldScaleBackground is removed, and you get snapPoints and showSwipeHandle props.
// Before
<Drawer direction="right">
// After
<Drawer swipeDirection="right">Other prop changes
| Component | Before | After |
|---|---|---|
| TooltipProvider | delayDuration | delay |
| Checkbox | checked="indeterminate" | indeterminate |
| Slider | onValueChange(value: number[]) | onValueChange(value: number | number[]) |
| NavigationMenu | viewport prop | removed, the viewport is always rendered |
| NavigationMenuLink | <Link legacyBehavior passHref> | render={<Link href="..." />} |
| DialogContent, SheetContent | - | showCloseButton prop |
All change handlers (onOpenChange, onValueChange, onCheckedChange) now receive an eventDetails object as the second argument.
Data attributes
If you styled components with Radix's data-state attribute, switch to Base UI's attributes.
| Before | After |
|---|---|
| data-[state=open] (content) | data-open |
| data-[state=closed] (content) | data-closed |
| data-[state=open] (trigger) | data-popup-open |
| data-[state=open] (accordion / collapsible trigger) | data-panel-open |
| data-[state=checked] / data-[state=unchecked] | data-checked / data-unchecked |
| data-[state=active] (tabs) | data-active |
| data-[active=true] (sidebar) | data-active |
| data-[highlighted] | data-highlighted |
| data-[disabled] | data-disabled |
| data-[motion=from-start] (navigation menu) | data-[activation-direction=left] |
| --radix-*-trigger-width | --anchor-width |
| --radix-*-content-transform-origin | --transform-origin |
Base UI also adds data-starting-style and data-ending-style for CSS transitions. Read the Base UI animation guide to learn more.
6. Update the dependencies
The components were built against the versions below. Some of the packages got a major version bump, so update them if you use the related components:
pnpm add lucide-react@latest react-day-picker@^9 date-fns@^4 recharts@^3 @tanstack/react-table@^9 react-resizable-panels@^4The rest of the packages only got minor updates and need no code changes, but it is a good idea to bring them up to date as well:
pnpm add tailwindcss@latest @tailwindcss/postcss@latest tw-animate-css@latest tailwind-merge@latest class-variance-authority@latest cmdk@latest input-otp@latest embla-carousel-react@latest react-hook-form@latest zod@^3| Component | Package | Version | What changed |
|---|---|---|---|
| All components | lucide-react | 0.477 to 1.x | Brand icons (Github, Twitter, Facebook...) were removed in 1.0. Replace them with another icon or your own SVG. |
| Calendar, Date Picker | react-day-picker | 8 to 9 | initialFocus is now autoFocus. Custom classNames keys use the v9 names. |
| Calendar, Date Picker | date-fns | 3 to 4 | No code changes for the components. Check the date-fns changelog if you use it elsewhere. |
| Chart | recharts | 2 to 3 | activeIndex / activeShape are replaced by the shape prop. layout moves from Bar to the chart. ChartLegend has no className, use ChartLegendContent. |
| Data Table | @tanstack/react-table | 8 to 9 | useReactTable is now useTable with a tableFeatures object. Columns are built with createColumnHelper and rendered with table.FlexRender. |
| Resizable | react-resizable-panels | 2 to 4 | The direction prop is now orientation. |
| Command | cmdk | 1.0 to 1.1 | No code changes. |
| Input OTP | input-otp | 1.2 to 1.5 | No code changes. |
| Carousel | embla-carousel-react | 8.0 to 8.6 | No code changes. |
| Form | react-hook-form, zod | 7.51 to 7.87, 3.22 to 3.25 | No code changes. |
| Styling | tailwindcss, @tailwindcss/postcss, tw-animate-css, tailwind-merge, class-variance-authority | 4.0 to 4.3, 1.2 to 1.4, 3.0 to 3.6, 0.7.0 to 0.7.1 | No code changes. |
Re-install these components from their pages to get the updated versions. Each component page has a short note about the upgrade.