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.
71
86%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
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.
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.@base-ui/react alongside radix. Radix packages are removed only
after the LAST component is migrated (both coexist fine).radix-<style>), the shadcn CLI itself is the golden-pair
executor:
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.components.json style radix-<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).shadcn add <component> --overwrite delivers 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.tsx instead.--overwrite would 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.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
against consumer-props.md.universal-patterns.md (imports
in BOTH forms: radix-ui and @radix-ui/react-*; asChild->render with the
worked example; Portal>Positioner>Popup; the positioner FORWARD rule; part
renames), the per-family props tables (overlays.md, menus.md,
form-controls.md, disclosure.md, display-misc.md), class-mapping.md
for data-attribute/CSS-var rewrites, and wrapper-shapes.md for exact
target shapes (tooltip arrow, SubContent defaults, select anatomy).Progressive (default). "Migrate accordion" = one component, strangler-fig:
<component>-base.tsx,
consumers split between old/new imports. The files ARE the state; resume,
never restart.<component>-base.tsx (original untouched;
golden-pair content fetched by URL, or transformed by hand, per the
strategy above); typecheck. Repoint consumers ONE AT A TIME (imports + the
call-site props in consumer-props.md); typecheck each. When no consumer
imports the original: delete it, rename -base -> original, flip imports
back, final check, commit. When the LAST radix wrapper in the project is
finalized, flip components.json to base-<style> and remove radix deps.Whole project (only when explicitly asked): same per-component work in
dependency order (leaf/shared wrappers like button and label first). After
wrappers, sweep ALL app code against consumer-props.md — the call-site
break surface is much larger than asChild. Then remove radix deps, install,
full build.
<label>; VisuallyHidden -> sr-only; Direction -> Direction
Provider (direction prop, not dir). Popover Anchor and NavigationMenu
Indicator have no equivalent: inert passthrough + flag.button.tsx migrates to the REAL @base-ui/react/button primitive, never
a hand-rolled useRender wrapper.Typecheck per file, build per batch, full build at the end vs the baseline.
Reports live in a .migration/ directory at the project root, ONE FILE PER
COMPONENT: .migration/<component>.md (e.g. .migration/accordion.md).
Rules:
.migration/project.md (dependency swap, app-code sweep summary, final
build result).ui alias from shadcn
info, e.g. components/ui or src/components/ui) for remaining radix imports
when asked "what's left". End every run's summary with that derived count
("N wrappers remain on Radix").Each .migration/<component>.md uses EXACTLY this structure (it is
documented publicly; reports must match it):
# <component>
<date, strategy used (golden pair via CLI / merge / engine), one-line verdict>
## Changed
<every file touched, with what changed and why; include file:line for
anything notable. Confirm the leftover scan is clean:
grep -n "radix-ui\|@radix-ui" on this component's files>
## Left alone
<files that look related but were intentionally not touched, with the reason
(cmdk/vaul/sonner are not radix; unrelated drift; etc.)>
## Behavior changes
<differences that compile fine but act differently; flagged, never patched
(tabs activation, menu close-on-click, delays...). Empty section if none>
## Verify by hand
<short manual QA checklist for this primitive family: focus return on
dialogs, keyboard nav + typeahead on menus/select, tooltip delay feel,
slider commit events. Concrete steps, one minute of clicking>98a1fe6
Also appears in
since Sep 16, 2026
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.