|
| 1 | +--- |
| 2 | +name: migrate-radix-to-base |
| 3 | +description: Migrates React projects and components from Radix UI to Base UI. Use when asked to migrate from radix, move to base-ui, convert radix primitives, or switch a shadcn project's base library. Handles single components ("migrate accordion") and whole projects. |
| 4 | +--- |
| 5 | + |
| 6 | +# Radix UI -> Base UI migration |
| 7 | + |
| 8 | +You migrate shadcn wrappers, hand-rolled radix compositions, and their |
| 9 | +consumers to `@base-ui/react`, keeping the project buildable at every step. |
| 10 | +Be precise; never guess a mapping. When a prop or part is not in these |
| 11 | +reference files, check `node_modules/@base-ui/react/**/*.d.ts` before |
| 12 | +transforming, and record gaps in the report. |
| 13 | + |
| 14 | +## Preflight (always) |
| 15 | + |
| 16 | +1. `npx shadcn@latest info --json` (or the project's runner): gives the |
| 17 | + current base, STYLE (e.g. `radix-lyra`), tailwind version, aliases, |
| 18 | + installed components, and package manager. Trust it over inference. |
| 19 | +2. Detect the package manager (packageManager field / lockfile: |
| 20 | + pnpm-lock.yaml, bun.lock, yarn.lock, package-lock.json) and use IT for |
| 21 | + every install. Never leave a stale lockfile. |
| 22 | +3. Require a clean git tree; work on a branch; one commit per component. |
| 23 | +4. Baseline check BEFORE touching dependencies: run the project's |
| 24 | + typecheck/build so pre-existing failures are never attributed to you. |
| 25 | +5. Install `@base-ui/react` alongside radix. Radix packages are removed only |
| 26 | + after the LAST component is migrated (both coexist fine). |
| 27 | + |
| 28 | +## Strategy: golden pair first, transformation engine second |
| 29 | + |
| 30 | +- **Golden pair via the CLI (preferred).** If the project is shadcn with a |
| 31 | + known style (`radix-<style>`), the shadcn CLI itself is the golden-pair |
| 32 | + executor: |
| 33 | + 1. Classify each ui wrapper FIRST: diff the user's file against its stock |
| 34 | + origin, using the components.json style VERBATIM in the URL |
| 35 | + (`https://ui.shadcn.com/r/styles/<style>/<component>.json`, |
| 36 | + files[0].content). This works for prefixed styles (radix-nova) AND |
| 37 | + legacy unprefixed ones (new-york, new-york-v4, default), which are all |
| 38 | + still served. |
| 39 | + 2. WHOLE-PROJECT mode: flip `components.json` style `radix-<style>` -> |
| 40 | + `base-<style>` now. PROGRESSIVE mode: do NOT flip yet (the project is |
| 41 | + still mostly radix; the flip happens once, after the last component); |
| 42 | + fetch base variants directly by URL instead |
| 43 | + (`https://ui.shadcn.com/r/styles/base-<style>/<component>.json`). |
| 44 | + 3. PRISTINE wrappers, whole-project mode: `shadcn add <component> |
| 45 | + --overwrite` delivers the base variant with the project's exact |
| 46 | + icon/font/preset resolution. Never bulk `--all --overwrite`; go |
| 47 | + component by component, or you drown in unrelated registry version |
| 48 | + drift. PROGRESSIVE mode: never use `--overwrite` (it destroys the |
| 49 | + original that consumers still import); write the fetched base variant |
| 50 | + content to `<component>-base.tsx` instead. |
| 51 | + 4. CUSTOMIZED wrappers: fetch the base variant and replay the user's diff |
| 52 | + onto it (their customizations must SURVIVE; `--overwrite` would destroy |
| 53 | + them). Mechanical implementation that works at scale: |
| 54 | + `git merge-file user.tsx radix-golden.tsx base-golden.tsx` (three-way |
| 55 | + merge, radix golden as ancestor) auto-resolves most files; hand-resolve |
| 56 | + conflicts with the reference tables. |
| 57 | + 5. MANDATORY leftover sweep on EVERY golden-pair file, including ones that |
| 58 | + merged "clean": `grep -n "radix-ui\|@radix-ui\|IconPlaceholder"` per |
| 59 | + file. The registry sometimes reorders functions between variants, which |
| 60 | + makes three-way merges report zero conflicts while leaving stale radix |
| 61 | + hunks in place. A clean merge is NOT proof of a clean file. |
| 62 | + This is more reliable than reconstructing transforms; use it whenever the |
| 63 | + pair exists. Consumer/app code has no CLI mechanism: always hand-migrate it |
| 64 | + against `consumer-props.md`. |
| 65 | +- **Legacy styles (new-york, new-york-v4, default): classification only, no |
| 66 | + replay.** These have no base counterpart (there is no base-new-york), and |
| 67 | + retargeting onto a base-<style> variant would restyle the user's app. Use |
| 68 | + the radix golden ONLY to detect customizations, then run the transformation |
| 69 | + engine on the user's OWN file: rewire primitives, keep their exact classes, |
| 70 | + apply class-mapping renames. Their look stays theirs. At the end of a |
| 71 | + legacy whole-project migration, FLAG (do not fix): the style name still |
| 72 | + reads as radix to the CLI, so future `shadcn add` will deliver radix |
| 73 | + variants; the user decides whether to switch style or add manually. |
| 74 | +- **Transformation engine (fallback).** Hand-rolled radix code, non-shadcn |
| 75 | + projects, unknown styles: transform using `universal-patterns.md` (imports |
| 76 | + in BOTH forms: `radix-ui` and `@radix-ui/react-*`; asChild->render with the |
| 77 | + worked example; Portal>Positioner>Popup; the positioner FORWARD rule; part |
| 78 | + renames), the per-family props tables (`overlays.md`, `menus.md`, |
| 79 | + `form-controls.md`, `disclosure.md`, `display-misc.md`), `class-mapping.md` |
| 80 | + for data-attribute/CSS-var rewrites, and `wrapper-shapes.md` for exact |
| 81 | + target shapes (tooltip arrow, SubContent defaults, select anatomy). |
| 82 | + |
| 83 | +## Modes |
| 84 | + |
| 85 | +**Progressive (default).** "Migrate accordion" = one component, strangler-fig: |
| 86 | +1. Detect in-progress state first: an existing `<component>-base.tsx`, |
| 87 | + consumers split between old/new imports. The files ARE the state; resume, |
| 88 | + never restart. |
| 89 | +2. If the component imports other ui wrappers still on radix (select -> |
| 90 | + button), STOP and recommend migrating those first, bottom-up. |
| 91 | +3. Write the migrated version to `<component>-base.tsx` (original untouched; |
| 92 | + golden-pair content fetched by URL, or transformed by hand, per the |
| 93 | + strategy above); typecheck. Repoint consumers ONE AT A TIME (imports + the |
| 94 | + call-site props in `consumer-props.md`); typecheck each. When no consumer |
| 95 | + imports the original: delete it, rename `-base` -> original, flip imports |
| 96 | + back, final check, commit. When the LAST radix wrapper in the project is |
| 97 | + finalized, flip `components.json` to `base-<style>` and remove radix deps. |
| 98 | + |
| 99 | +**Whole project** (only when explicitly asked): same per-component work in |
| 100 | +dependency order (leaf/shared wrappers like button and label first). After |
| 101 | +wrappers, sweep ALL app code against `consumer-props.md` — the call-site |
| 102 | +break surface is much larger than asChild. Then remove radix deps, install, |
| 103 | +full build. |
| 104 | + |
| 105 | +## Hard rules |
| 106 | + |
| 107 | +- NEVER touch non-radix libraries or their wrappers: cmdk (command), vaul |
| 108 | + (drawer), sonner, input-otp, react-day-picker (calendar), recharts (chart). |
| 109 | + Report them as intentionally untouched. |
| 110 | +- No Base UI counterpart: AspectRatio -> CSS aspect-ratio div; Label -> |
| 111 | + native `<label>`; VisuallyHidden -> `sr-only`; Direction -> Direction |
| 112 | + Provider (`direction` prop, not `dir`). Popover Anchor and NavigationMenu |
| 113 | + Indicator have no equivalent: inert passthrough + flag. |
| 114 | +- `button.tsx` migrates to the REAL `@base-ui/react/button` primitive, never |
| 115 | + a hand-rolled useRender wrapper. |
| 116 | +- Behavior deltas are FLAGGED, never silently patched (tabs manual |
| 117 | + activation, menu items not closing on click, nav-menu 50ms delay). The |
| 118 | + target is idiomatic Base UI matching the shadcn base registry. |
| 119 | +- Honest reporting: skipped/reverted files are listed as flagged, never as |
| 120 | + migrated. Pre-existing failures are named as pre-existing. |
| 121 | + |
| 122 | +## Verify and report |
| 123 | + |
| 124 | +Typecheck per file, build per batch, full build at the end vs the baseline. |
| 125 | + |
| 126 | +Reports live in a `.migration/` directory at the project root, ONE FILE PER |
| 127 | +COMPONENT: `.migration/<component>.md` (e.g. `.migration/accordion.md`). |
| 128 | +Rules: |
| 129 | +- Each run writes (or fully overwrites) the file for each component it |
| 130 | + migrated. Re-running a component replaces its report; never touch other |
| 131 | + components' files. |
| 132 | +- A multi-component run ("migrate alert-dialog and dropdown-menu") writes one |
| 133 | + file per component, each self-contained; shared consumer-sweep notes are |
| 134 | + repeated in every affected file. |
| 135 | +- Whole-project mode writes the per-component files plus |
| 136 | + `.migration/project.md` (dependency swap, app-code sweep summary, final |
| 137 | + build result). |
| 138 | +- There is NO index file. Migration status is derived from disk, not |
| 139 | + maintained: scan the project's ui directory (the `ui` alias from shadcn |
| 140 | + info, e.g. components/ui or src/components/ui) for remaining radix imports |
| 141 | + when asked "what's left". End every run's summary with that derived count |
| 142 | + ("N wrappers remain on Radix"). |
| 143 | + |
| 144 | +Each `.migration/<component>.md` uses EXACTLY this structure (it is |
| 145 | +documented publicly; reports must match it): |
| 146 | + |
| 147 | +```md |
| 148 | +# <component> |
| 149 | + |
| 150 | +<date, strategy used (golden pair via CLI / merge / engine), one-line verdict> |
| 151 | + |
| 152 | +## Changed |
| 153 | + |
| 154 | +<every file touched, with what changed and why; include file:line for |
| 155 | +anything notable. Confirm the leftover scan is clean: |
| 156 | +grep -n "radix-ui\|@radix-ui" on this component's files> |
| 157 | + |
| 158 | +## Left alone |
| 159 | + |
| 160 | +<files that look related but were intentionally not touched, with the reason |
| 161 | +(cmdk/vaul/sonner are not radix; unrelated drift; etc.)> |
| 162 | + |
| 163 | +## Behavior changes |
| 164 | + |
| 165 | +<differences that compile fine but act differently; flagged, never patched |
| 166 | +(tabs activation, menu close-on-click, delays...). Empty section if none> |
| 167 | + |
| 168 | +## Verify by hand |
| 169 | + |
| 170 | +<short manual QA checklist for this primitive family: focus return on |
| 171 | +dialogs, keyboard nav + typeahead on menus/select, tooltip delay feel, |
| 172 | +slider commit events. Concrete steps, one minute of clicking> |
| 173 | +``` |
0 commit comments