Most developers I talk to think moving from Radix UI to Base UI in shadcn means running an automated codemod, wiping the components/ui directory, or letting a script refactor every component in one click. That mental model works until it does not, and when it breaks in a live project with dozens of forms, dialogs, and tables, the resulting build errors are difficult to untangle. This article explains what the Radix to Base UI shift in shadcn actually represents, why shadcn deliberately avoided building an AST codemod, and how the mechanics of component ownership dictate your upgrade path.
What It Is
When shadcn announced Base UI as the default component library, the immediate reaction across the ecosystem was to look for a migration script. But shadcn is not an npm package you import as an immutable dependency; it is code copied directly into your repository. You own the component files in your project. You add variants, change class names, wire custom handlers, and inject project-specific helpers.
Because of this architecture, a traditional AST codemod cannot reliably transform your code. A codemod works well on pristine templates, but it fails the moment it encounters human customizations. Instead of shipping a brittle script, shadcn introduced two mechanisms: multi-base support in the CLI for initializing or reinstalling styled primitives, and a dedicated AI migration skill that reads both implementations, compares the diffs against pristine golden pairs, and carries your customizations forward progressively.
The Mental Model: Tailoring a Bespoke Suit
Think of your shadcn component directory like a collection of tailored suits rather than off-the-rack clothing. Radix provided the initial fabric and pattern. Over months of building your application, you hemmed the pants, adjusted the sleeves, and added custom pockets.
A traditional codemod acts like an automated cutting machine programmed strictly for the original factory pattern. If you run it over a tailored suit, it cuts straight through your custom seams and ruins the garment. To change the underlying fabric to Base UI without destroying the tailoring, you must measure your specific alterations, understand how the new material behaves, and apply the adjustments seam by seam.
In code, this difference is immediately visible at the boundary between primitive wrappers and consumer call sites. In Radix, polymorphism is driven by the asChild prop:
Because asChild expects a single React element passed as a child while render accepts an element or a render function directly on the prop, changing the primitive under the hood ripples out to every call site in your application that relied on the previous convention.
When To Use It
Reaching for Base UI or switching your project base makes sense in very specific situations.
If you are bootstrapping a new project or building a greenfield feature set where you want modern primitive features like built-in field validation states, native transition hooks, or unstyled positioning that separates the viewport from the popup box, Base UI is the right default.
It is also appropriate if you are maintaining an existing project and decide to execute a deliberate, progressive component migration using the official skill. In that scenario, you migrate foundational leaf primitives first, such as buttons and labels, verify that your application builds cleanly after each step, and allow both libraries to coexist peacefully during the transition.
Before undertaking a primitive upgrade across your UI layer, make sure you actually need to own the underlying behavior. If you are weighing whether to adopt headless primitives like Radix and Base UI or keep hand-rolling interaction logic, read Headless UI Primitives: When to Own UI Behavior Today. It outlines a pragmatic framework for identifying invisible interaction risks—such as focus traps, keyboard navigation, and ARIA state—before committing engineering time to component architecture.
When NOT To Use It
You should avoid migrating to Base UI if your production application is already stable, thoroughly tested, and running on Radix primitives without issue.
Radix UI is not deprecated. It remains officially supported by shadcn, and its primitives have been battle-tested across millions of production interfaces. If you have an established app with dozens of complex dialogs, navigation menus, and form controls, migrating simply because the default changed adds risk with very little tangible benefit to your end users.
Equally important, never attempt to migrate by deleting your components/ui folder and running an unconditional CLI re-initialization. Doing so wipes out all custom variants, resets accessibility adjustments, and immediately introduces breaking type mismatches across every page in your repository.
Gotchas and Common Mistakes
The biggest gotcha in this process is collateral file overwrites by the shadcn CLI. When you run shadcn init --reinstall to pull down a new preset or re-sync components, the CLI does not just touch files inside src/components/ui. It also inspects and overwrites your utility file at src/lib/utils.ts.
In a real codebase, utils.ts frequently holds essential application helpers like retry logic, query string parsers, or formatting functions. The CLI unconditionally replaces the entire file with a single export:
typescript
// src/lib/utils.tsexport { cn } from"cn"
If you do not review your git diff immediately after running the command, entire sections of your business logic will fail to compile because their shared utilities were quietly deleted.
Another subtle trap involves call-site prop drift on third-party calendar components. In older shadcn setups, date pickers commonly passed initialFocus to the calendar wrapper. When updating components alongside dependencies like react-day-picker version 10, that prop disappears from the interface, causing cryptic type errors during production builds:
tsx
// src/app/(admin)/admin/dobavnice/_components/delivery-note-filters.tsx
<Calendar
mode="range"
selected={formState.dateRange}
initialFocus // Breaks build: property does not exist in react-day-picker v10
/>
Resolving this requires handling the prop bridge within the wrapper itself or mapping it to autoFocus before running your production build.
Finally, be wary of structural differences in menu and overlay primitives. In Radix, a popover or select menu handled collisions, positioning, and content in a single Content component. Base UI splits this into three distinct parts: Portal, Positioner, and Popup. If you swap wrappers without accounting for how your application passes alignment offsets or manages portal containers, your dropdowns and modals will compile but render detached from their triggers.
Conclusion
The transition from Radix UI to Base UI in shadcn represents a fundamental shift in how modern headless components are structured, but it does not mean your existing codebase needs an emergency rewrite. You own your code, which makes automated codemods impractical and progressive component adoption the only reliable path forward. Reach for Base UI when building new projects or when a specific primitive feature solves an active problem, but leave your stable Radix foundations alone if your application is already shipping reliably.
If you have questions or ran into a different gotcha, drop a comment below. And if you found this useful, subscribe for more.