Radix UI -> Base UI migration
You migrate shadcn wrappers, hand-rolled radix compositions, and their
consumers to @base-ui/react, keeping the project buildable at every step.
Be precise; never guess a mapping. When a prop or part is not in these
reference files, check node_modules/@base-ui/react/**/*.d.ts before
transforming, and record gaps in the report.
Preflight (always)
npx shadcn@latest info --json(or the project's runner): gives the current base, STYLE (e.g.radix-lyra), tailwind version, aliases, installed components, and package manager. Trust it over inference.- Detect the package manager (packageManager field / lockfile: pnpm-lock.yaml, bun.lock, yarn.lock, package-lock.json) and use IT for every install. Never leave a stale lockfile.
- Require a clean git tree; work on a branch; one commit per component.
- Baseline check BEFORE touching dependencies: run the project's typecheck/build so pre-existing failures are never attributed to you.
- Install
@base-ui/reactalongside radix. Radix packages are removed only after the LAST component is migrated (both coexist fine).
Strategy: golden pair first, transformation engine second
- Golden pair via the CLI (preferred). If the project is shadcn with a
known style (
radix-<style>), the shadcn CLI itself is the golden-pair executor:- Classify each ui wrapper FIRST: diff the user's file against its stock
origin, using the components.json style VERBATIM in the URL
(
https://ui.shadcn.com/r/styles/<style>/<component>.json, files[0].content). This works for prefixed styles (radix-nova) AND legacy unprefixed ones (new-york, new-york-v4, default), which are all still served. - WHOLE-PROJECT mode: flip
components.jsonstyleradix-<style>->base-<style>now. PROGRESSIVE mode: do NOT flip yet (the project is still mostly radix; the flip happens once, after the last component); fetch base variants directly by URL instead (https://ui.shadcn.com/r/styles/base-<style>/<component>.json). - PRISTINE wrappers, whole-project mode:
shadcn add <component> --overwritedelivers the base variant with the project's exact icon/font/preset resolution. Never bulk--all --overwrite; go component by component, or you drown in unrelated registry version drift. PROGRESSIVE mode: never use--overwrite(it destroys the original that consumers still import); write the fetched base variant content to<component>-base.tsxinstead. - CUSTOMIZED wrappers: fetch the base variant and replay the user's diff
onto it (their customizations must SURVIVE;
--overwritewould destroy them). Mechanical implementation that works at scale:git merge-file user.tsx radix-golden.tsx base-golden.tsx(three-way merge, radix golden as ancestor) auto-resolves most files; hand-resolve conflicts with the reference tables. - MANDATORY leftover sweep on EVERY golden-pair file, including ones that
merged "clean":
grep -n "radix-ui\|@radix-ui\|IconPlaceholder"per file. The registry sometimes reorders functions between variants, which makes three-way merges report zero conflicts while leaving stale radix hunks in place. A clean merge is NOT proof of a clean file. This is more reliable than reconstructing transforms; use it whenever the pair exists. Consumer/app code has no CLI mechanism: always hand-migrate it againstconsumer-props.md.
- Classify each ui wrapper FIRST: diff the user's file against its stock
origin, using the components.json style VERBATIM in the URL
(
- Legacy styles (new-york, new-york-v4, default): classification only, no replay. These have no base counterpart (there is no base-new-york), and retargeting onto a base-