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 asChild prop is gone. Every component that supported it now accepts Base UI's render prop.
  • The drawer component now uses Base UI's drawer instead of vaul.
  • Popup components (dropdown menu, popover, tooltip, select...) accept side, sideOffset, align and alignOffset directly on their content component.
  • State is exposed through Base UI's data attributes (data-open, data-checked, data-active...) instead of Radix's data-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/react

If you are starting a new project, initialize shadcn with Base UI as the component library:


pnpm dlx shadcn@latest init -b base

2. 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-themes

5. 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 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

ComponentBeforeAfter
TooltipProviderdelayDurationdelay
Checkboxchecked="indeterminate"indeterminate
SlideronValueChange(value: number[])onValueChange(value: number | number[])
NavigationMenuviewport propremoved, 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.


BeforeAfter
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@^4

The 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
ComponentPackageVersionWhat changed
All componentslucide-react0.477 to 1.xBrand icons (Github, Twitter, Facebook...) were removed in 1.0. Replace them with another icon or your own SVG.
Calendar, Date Pickerreact-day-picker8 to 9initialFocus is now autoFocus. Custom classNames keys use the v9 names.
Calendar, Date Pickerdate-fns3 to 4No code changes for the components. Check the date-fns changelog if you use it elsewhere.
Chartrecharts2 to 3activeIndex / activeShape are replaced by the shape prop. layout moves from Bar to the chart. ChartLegend has no className, use ChartLegendContent.
Data Table@tanstack/react-table8 to 9useReactTable is now useTable with a tableFeatures object. Columns are built with createColumnHelper and rendered with table.FlexRender.
Resizablereact-resizable-panels2 to 4The direction prop is now orientation.
Commandcmdk1.0 to 1.1No code changes.
Input OTPinput-otp1.2 to 1.5No code changes.
Carouselembla-carousel-react8.0 to 8.6No code changes.
Formreact-hook-form, zod7.51 to 7.87, 3.22 to 3.25No code changes.
Stylingtailwindcss, @tailwindcss/postcss, tw-animate-css, tailwind-merge, class-variance-authority4.0 to 4.3, 1.2 to 1.4, 3.0 to 3.6, 0.7.0 to 0.7.1No code changes.

Re-install these components from their pages to get the updated versions. Each component page has a short note about the upgrade.