migrate-radix-to-base

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Radix UI -> Base UI migration

Radix UI 迁移至 Base UI

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.
你需要将shadcn包装器、手动编写的radix组合及其使用者迁移至
@base-ui/react
,确保项目在每一步都可构建。务必精准,切勿猜测映射关系。当某个属性或部分不在参考文件中时,在进行转换前先检查
node_modules/@base-ui/react/**/*.d.ts
,并在报告中记录缺失内容。

Preflight (always)

预检步骤(必须执行)

  1. 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.
  2. 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.
  3. Require a clean git tree; work on a branch; one commit per component.
  4. Baseline check BEFORE touching dependencies: run the project's typecheck/build so pre-existing failures are never attributed to you.
  5. Install
    @base-ui/react
    alongside radix. Radix packages are removed only after the LAST component is migrated (both coexist fine).
  1. 执行
    npx shadcn@latest info --json
    (或项目的运行器):获取当前基础库、样式(如
    radix-lyra
    )、tailwind版本、别名、已安装组件和包管理器。信任该命令的输出而非推断结果。
  2. 检测包管理器(通过packageManager字段或锁文件:pnpm-lock.yaml、bun.lock、yarn.lock、package-lock.json),并始终使用该包管理器进行安装操作,切勿留下过时的锁文件。
  3. 要求git工作区处于干净状态;在分支上进行操作;每个组件对应一次提交。
  4. 在修改依赖前进行基线检查:运行项目的类型检查/构建命令,确保预先存在的错误不会被归咎于本次迁移。
  5. 在radix旁边安装
    @base-ui/react
    。仅在最后一个组件完成迁移后再移除radix包(两者可以共存)。

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:
    1. 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.
    2. WHOLE-PROJECT mode: flip
      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
      ).
    3. PRISTINE wrappers, whole-project mode:
      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.
    4. CUSTOMIZED wrappers: fetch the base variant and replay the user's diff onto it (their customizations must SURVIVE;
      --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.
    5. 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 against
      consumer-props.md
      .
  • 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-<style> variant would restyle the user's app. Use the radix golden ONLY to detect customizations, then run the transformation engine on the user's OWN file: rewire primitives, keep their exact classes, apply class-mapping renames. Their look stays theirs. At the end of a legacy whole-project migration, FLAG (do not fix): the style name still reads as radix to the CLI, so future
    shadcn add
    will deliver radix variants; the user decides whether to switch style or add manually.
  • Transformation engine (fallback). Hand-rolled radix code, non-shadcn projects, unknown styles: transform using
    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).
  • 优先通过CLI实现黄金配对。如果项目是带有已知样式(
    radix-<style>
    )的shadcn项目,shadcn CLI本身就是黄金配对的执行工具:
    1. 首先对每个ui包装器进行分类:将用户文件与其原始模板进行对比,使用components.json中指定的样式直接访问URL(
      https://ui.shadcn.com/r/styles/<style>/<component>.json
      ,取files[0].content)。这适用于带前缀的样式(radix-nova)和旧版无前缀样式(new-york、new-york-v4、default),这些样式仍可正常访问。
    2. 全项目模式:立即将
      components.json
      中的样式从
      radix-<style>
      修改为
      base-<style>
      渐进模式:暂不修改样式(项目仍以radix为主;仅在最后一个组件迁移完成后再一次性修改),直接通过URL获取base版本的组件(
      https://ui.shadcn.com/r/styles/base-<style>/<component>.json
      )。
    3. 纯净包装器(全项目模式):执行
      shadcn add <component> --overwrite
      即可获取适配项目图标/字体/预设的base版本组件。切勿批量执行
      --all --overwrite
      ,应逐个组件处理,否则会陷入无关的注册表版本漂移问题。渐进模式:切勿使用
      --overwrite
      (会破坏仍被使用者导入的原始文件),而是将获取到的base版本内容写入
      <component>-base.tsx
      文件。
    4. 自定义包装器:获取base版本组件,并将用户的自定义修改应用到该版本上(必须保留用户的自定义内容;
      --overwrite
      会破坏这些内容)。可规模化实现的机械方式:使用
      git merge-file user.tsx radix-golden.tsx base-golden.tsx
      (三方合并,以radix模板为祖先)自动解析大多数文件;对于冲突部分,参考对照表手动解决。
    5. 对所有黄金配对文件进行强制扫尾检查,包括那些“干净合并”的文件:对每个文件执行
      grep -n "radix-ui\|@radix-ui\|IconPlaceholder"
      。注册表有时会在不同版本间重新排序函数,导致三方合并报告无冲突,但文件中仍残留radix相关代码块。干净的合并并不代表文件已清理完毕。 这种方式比重构转换逻辑更可靠,只要存在配对模板就应优先使用。消费者/应用代码没有对应的CLI机制:需始终对照
      consumer-props.md
      手动迁移。
  • 旧版样式(new-york、new-york-v4、default):仅分类,不重放。这些样式没有对应的base版本(不存在base-new-york),将其重定向到base-<style>版本会改变用户应用的样式。仅使用radix模板检测自定义内容,然后对用户自己的文件运行转换引擎:重新连接基础组件,保留用户的精确类名,应用类名映射重命名。确保应用外观保持不变。在旧版样式的全项目迁移结束后,需标记(而非修复):样式名称在CLI中仍显示为radix,因此未来执行
    shadcn add
    会获取radix版本组件;由用户决定是否切换样式或手动添加组件。
  • 转换引擎(备选方案)。手动编写的radix代码、非shadcn项目、未知样式:使用
    universal-patterns.md
    (两种导入形式:
    radix-ui
    @radix-ui/react-*
    ;asChild转换为render并参考示例;Portal>Positioner>Popup;定位器转发规则;部件重命名)、各组件族的属性表(
    overlays.md
    menus.md
    form-controls.md
    disclosure.md
    display-misc.md
    )、
    class-mapping.md
    用于数据属性/CSS变量重写,以及
    wrapper-shapes.md
    用于精确的目标结构(tooltip箭头、SubContent默认值、select结构)进行转换。

Modes

模式

Progressive (default). "Migrate accordion" = one component, strangler-fig:
  1. Detect in-progress state first: an existing
    <component>-base.tsx
    , consumers split between old/new imports. The files ARE the state; resume, never restart.
  2. If the component imports other ui wrappers still on radix (select -> button), STOP and recommend migrating those first, bottom-up.
  3. Write the migrated version to
    <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.
渐进模式(默认)。“迁移accordion”即迁移单个组件,采用绞杀者模式:
  1. 首先检测进行中的状态:是否存在已有的
    <component>-base.tsx
    文件,使用者是否同时导入旧版和新版组件。文件本身即为状态标记;应继续迁移,切勿重新开始。
  2. 如果当前组件导入了仍基于radix的其他ui包装器(如select依赖button),则停止迁移并建议先迁移这些依赖组件,采用自底向上的方式。
  3. 将迁移后的版本写入
    <component>-base.tsx
    (原始文件保持不变;根据上述策略,通过URL获取黄金配对内容或手动转换);执行类型检查。逐个重新指向使用者(导入路径 +
    consumer-props.md
    中的调用属性);每次修改后都执行类型检查。当没有使用者再导入原始文件时:删除原始文件,将
    -base
    后缀的文件重命名为原始文件名,恢复导入路径,进行最终检查后提交。当项目中最后一个radix包装器完成迁移后,将
    components.json
    中的样式修改为
    base-<style>
    并移除radix依赖。
全项目模式(仅在明确要求时使用):按照依赖顺序逐个组件处理(先处理叶子/共享包装器,如button和label)。完成包装器迁移后,对照
consumer-props.md
扫描所有应用代码——调用端的修改面远大于asChild。然后移除radix依赖,执行安装和完整构建。

Hard rules

硬性规则

  • NEVER touch non-radix libraries or their wrappers: cmdk (command), vaul (drawer), sonner, input-otp, react-day-picker (calendar), recharts (chart). Report them as intentionally untouched.
  • No Base UI counterpart: AspectRatio -> CSS aspect-ratio div; Label -> native
    <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.
  • Behavior deltas are FLAGGED, never silently patched (tabs manual activation, menu items not closing on click, nav-menu 50ms delay). The target is idiomatic Base UI matching the shadcn base registry.
  • Honest reporting: skipped/reverted files are listed as flagged, never as migrated. Pre-existing failures are named as pre-existing.
  • 切勿修改非radix库或其包装器:cmdk(命令组件)、vaul(抽屉组件)、sonner、input-otp、react-day-picker(日历)、recharts(图表)。需将这些组件列为故意未修改的内容。
  • 无Base UI对应组件的处理方式:AspectRatio -> 使用CSS aspect-ratio属性的div;Label -> 原生
    <label>
    ;VisuallyHidden ->
    sr-only
    类;Direction -> Direction Provider(使用
    direction
    属性,而非
    dir
    )。Popover Anchor和NavigationMenu Indicator无对应组件:保留惰性透传并标记。
  • button.tsx
    需迁移至真正的
    @base-ui/react/button
    基础组件,切勿使用手动编写的useRender包装器。
  • 行为差异需标记,切勿静默修复(如tabs手动激活、菜单项点击不关闭、导航菜单50ms延迟)。目标是符合shadcn base注册表的Base UI惯用写法。
  • 如实报告:跳过/回滚的文件需列为标记内容,而非已迁移内容。预先存在的错误需明确标注为预先存在。

Verify and report

验证与报告

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:
  • Each run writes (or fully overwrites) the file for each component it migrated. Re-running a component replaces its report; never touch other components' files.
  • A multi-component run ("migrate alert-dialog and dropdown-menu") writes one file per component, each self-contained; shared consumer-sweep notes are repeated in every affected file.
  • Whole-project mode writes the per-component files plus
    .migration/project.md
    (dependency swap, app-code sweep summary, final build result).
  • There is NO index file. Migration status is derived from disk, not maintained: scan the project's ui directory (the
    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):
md
undefined
每个文件都要执行类型检查,每批组件迁移后执行构建,最终与基线对比执行完整构建。
报告存储在项目根目录的
.migration/
目录中,每个组件对应一个文件
.migration/<component>.md
(例如
.migration/accordion.md
)。规则如下:
  • 每次运行都会写入(或完全覆盖)每个已迁移组件的报告文件。重新运行组件迁移会替换其报告;切勿修改其他组件的报告文件。
  • 多组件迁移(如“迁移alert-dialog和dropdown-menu”)会为每个组件生成一个独立文件;共享的使用者扫描说明需在每个受影响的文件中重复。
  • 全项目模式会生成每个组件的报告文件,外加
    .migration/project.md
    (依赖替换、应用代码扫描总结、最终构建结果)。
  • 不生成索引文件。迁移状态从磁盘文件推导,而非手动维护:当被问及“还剩哪些未迁移”时,扫描项目的ui目录(来自shadcn info的
    ui
    别名,如components/ui或src/components/ui)中剩余的radix导入。每次运行的总结末尾需包含该推导计数(“仍有N个包装器基于Radix”)。
每个
.migration/<component>.md
必须严格遵循以下结构(该结构已公开文档化;报告必须与之匹配):
md
undefined

<component>

<component>

<date, strategy used (golden pair via CLI / merge / engine), one-line verdict>
<date, strategy used (golden pair via CLI / merge / engine), one-line verdict>

Changed

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

Left alone

<files that look related but were intentionally not touched, with the reason (cmdk/vaul/sonner are not radix; unrelated drift; etc.)>
<files that look related but were intentionally not touched, with the reason (cmdk/vaul/sonner are not radix; unrelated drift; etc.)>

Behavior changes

Behavior changes

<differences that compile fine but act differently; flagged, never patched (tabs activation, menu close-on-click, delays...). Empty section if none>
<differences that compile fine but act differently; flagged, never patched (tabs activation, menu close-on-click, delays...). Empty section if none>

Verify by hand

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>
undefined
<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>
undefined