/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
文章目录
-
- 一、先破除六个误会
- 二、它到底是什么:一张分层架构图
- 三、5 分钟最小可跑示例(Vite + React + Tailwind v4)
-
- 3.1 建工程、装 Tailwind v4
- 3.2 初始化
- 3.3 添加组件
- 3.4 用起来
- 四、CLI v4 命令速查(2026 年最该记住的一节)
- 五、三大支柱:为什么它「看起来简单,用起来稳」
-
- 5.1 行为层:三个基座怎么选
- 5.2 样式层:Tailwind + CVA
- 5.3 覆盖层:`cn` 与 tailwind-merge(2026-09 有重要变更)
- 六、主题与深色模式:CSS 变量才是核心
- 七、新手必踩的七个坑
- 八、选型实战:shadcn/ui 到底该不该用
-
- 8.1 四方案正面对比
- 8.2 决策树
- 8.3 我的结论:分层使用,而不是全盘接管
- 九、FAQ
- 十、总结
- 参考与延伸阅读

摘要:很多 shadcn/ui 教程第一句就写错了:「shadcn/ui 是一个基于 Radix UI 的 React 组件库」——这句话在 2026 年已经两处过时。 它不是组件库,也没有可 import 的 npm 包;而它的默认底层原语,已经在 2026 年 7 月从 Radix 换成了 Base UI。本文分三层讲清:① 破除六个流行误会,讲透它的本质是「registry + CLI + 源码所有权」;② 用 Vite + React + Tailwind v4 跑通一个 5 分钟最小示例,并讲清三大支柱(行为原语 / Tailwind 样式 / cn 覆盖);③ 站在选型视角,对比 shadcn/ui、MUI、Ant Design 与「裸用无头原语」,给出决策树与一个不站队的混合结论。全文含 CLI v4 命令速查表与七个新手必踩的坑。读完你应该能回答两个问题:怎么上手,以及它值不值得我为「掌控权」付出维护成本。
关键词:shadcn/ui、Base UI、Radix UI、React Aria、Tailwind CSS v4、CVA、无头组件、组件库选型、Design System
适合人群:正在给 React / Next.js 项目选组件方案的前端开发者与架构师;试用过 shadcn/ui 但总搞不清「它和 MUI 到底差在哪」的同学;以及被「Radix 要废弃了吗」这类传言搞焦虑的人。
一、先破除六个误会
关于 shadcn/ui 的误解密度,比大部分技术话题都高。先把这六条摆正,后面的讨论才有共同语言。
| 「它是一个组件库」 | 它不是库。没有 import { Button } from 'shadcn' 这种东西。它是 CLI + registry,把组件源码复制进你的仓库 | 根本误判 |
| 「它基于 Radix UI」 | 这是 2023 年的事实。2026 年 7 月起,新项目默认用 Base UI;Radix 未被废弃,另有 React Aria 作为第三选项 | 已过时 |
| 「装完就能一直自动升级」 | 复制即冻结。上游修了 bug 不会自动到达你的仓库,需要 –diff 手动比对合并 | 期望偏差 |
| 「什么框架都能用」 | 官方主线是 React(Next.js / Vite / React Router / Astro / TanStack Start / Laravel)。Vue、Svelte、Solid 是社区移植版,不是同一份代码 | 概念混淆 |
| 「npm install shadcn-ui」 | 旧包已废弃。现在的包名是 shadcn,命令统一为 npx shadcn@latest | 教程陈旧 |
| 「不用 Tailwind 也能用」 | 不能。组件本体就是 Tailwind 类名,没有 Tailwind 就没有样式 | 硬性前提 |
一句话总结:shadcn/ui 是一套「把可访问的组件源码分发给你」的机制。 它交付的不是能力,而是代码的所有权。这个所有权,正是它能绕过传统组件库那道「你改不动 node_modules 里的东西」的墙的原因。
二、它到底是什么:一张分层架构图
#mermaid-svg-A8pkG0E8UUaH2MMT{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-A8pkG0E8UUaH2MMT .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-A8pkG0E8UUaH2MMT .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-A8pkG0E8UUaH2MMT .error-icon{fill:#552222;}#mermaid-svg-A8pkG0E8UUaH2MMT .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-A8pkG0E8UUaH2MMT .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-A8pkG0E8UUaH2MMT .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-A8pkG0E8UUaH2MMT .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-A8pkG0E8UUaH2MMT .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-A8pkG0E8UUaH2MMT .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-A8pkG0E8UUaH2MMT .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-A8pkG0E8UUaH2MMT .marker{fill:#333333;stroke:#333333;}#mermaid-svg-A8pkG0E8UUaH2MMT .marker.cross{stroke:#333333;}#mermaid-svg-A8pkG0E8UUaH2MMT svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-A8pkG0E8UUaH2MMT p{margin:0;}#mermaid-svg-A8pkG0E8UUaH2MMT .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-A8pkG0E8UUaH2MMT .cluster-label text{fill:#333;}#mermaid-svg-A8pkG0E8UUaH2MMT .cluster-label span{color:#333;}#mermaid-svg-A8pkG0E8UUaH2MMT .cluster-label span p{background-color:transparent;}#mermaid-svg-A8pkG0E8UUaH2MMT .label text,#mermaid-svg-A8pkG0E8UUaH2MMT span{fill:#333;color:#333;}#mermaid-svg-A8pkG0E8UUaH2MMT .node rect,#mermaid-svg-A8pkG0E8UUaH2MMT .node circle,#mermaid-svg-A8pkG0E8UUaH2MMT .node ellipse,#mermaid-svg-A8pkG0E8UUaH2MMT .node polygon,#mermaid-svg-A8pkG0E8UUaH2MMT .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-A8pkG0E8UUaH2MMT .rough-node .label text,#mermaid-svg-A8pkG0E8UUaH2MMT .node .label text,#mermaid-svg-A8pkG0E8UUaH2MMT .image-shape .label,#mermaid-svg-A8pkG0E8UUaH2MMT .icon-shape .label{text-anchor:middle;}#mermaid-svg-A8pkG0E8UUaH2MMT .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-A8pkG0E8UUaH2MMT .rough-node .label,#mermaid-svg-A8pkG0E8UUaH2MMT .node .label,#mermaid-svg-A8pkG0E8UUaH2MMT .image-shape .label,#mermaid-svg-A8pkG0E8UUaH2MMT .icon-shape .label{text-align:center;}#mermaid-svg-A8pkG0E8UUaH2MMT .node.clickable{cursor:pointer;}#mermaid-svg-A8pkG0E8UUaH2MMT .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-A8pkG0E8UUaH2MMT .arrowheadPath{fill:#333333;}#mermaid-svg-A8pkG0E8UUaH2MMT .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-A8pkG0E8UUaH2MMT .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-A8pkG0E8UUaH2MMT .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-A8pkG0E8UUaH2MMT .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-A8pkG0E8UUaH2MMT .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-A8pkG0E8UUaH2MMT .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-A8pkG0E8UUaH2MMT .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-A8pkG0E8UUaH2MMT .cluster text{fill:#333;}#mermaid-svg-A8pkG0E8UUaH2MMT .cluster span{color:#333;}#mermaid-svg-A8pkG0E8UUaH2MMT div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-A8pkG0E8UUaH2MMT .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-A8pkG0E8UUaH2MMT rect.text{fill:none;stroke-width:0;}#mermaid-svg-A8pkG0E8UUaH2MMT .icon-shape,#mermaid-svg-A8pkG0E8UUaH2MMT .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-A8pkG0E8UUaH2MMT .icon-shape p,#mermaid-svg-A8pkG0E8UUaH2MMT .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-A8pkG0E8UUaH2MMT .icon-shape .label rect,#mermaid-svg-A8pkG0E8UUaH2MMT .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-A8pkG0E8UUaH2MMT .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-A8pkG0E8UUaH2MMT .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-A8pkG0E8UUaH2MMT :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
依赖层(这里才是真正的 npm 依赖)
你的仓库 —— 这才是真正的产物
shadcn CLI
init / add / diff / –dry-run / view / migrate
远端 Registry(ui.shadcn.com 或你自己的私有 registry)
registry-item.json描述:源码 + 依赖 + CSS 变量 + 字体
src/components/ui/button.tsx← 源码复制进来,逐行归你所有
components.jsonbase / 风格 / 别名 / 图标库
CSS 变量主题–primary / –background / .dark
行为原语Base UI(默认)/ Radix / React Aria
样式:Tailwind CSS + CVA
类名合并:cn 包
看懂这张图,你立刻能理解三件事:
三、5 分钟最小可跑示例(Vite + React + Tailwind v4)
3.1 建工程、装 Tailwind v4
npm create vite@latest shadcn-demo — –template react-ts
cd shadcn-demo
npm install
# shadcn/ui 的硬性前提:先把 Tailwind v4 跑通
npm install tailwindcss @tailwindcss/vite
// vite.config.ts
import path from 'node:path'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
// 关键:不配 @ 别名,shadcn init 会找不到组件目录
alias: { '@': path.resolve(__dirname, './src') },
},
})
// tsconfig.json(tsconfig.app.json 同理)
{
"compilerOptions": {
"baseUrl": ".",
"paths": { "@/*": ["./src/*"] }
}
}
/* src/index.css */
@import "tailwindcss";
这三处别名必须一致(vite 的 resolve.alias + tsconfig 的 paths)。它是新手第一次 init 失败最常见的原因——CLI 会提示找不到 components 路径,或者组件里的 @/lib/utils 一片红。
3.2 初始化
npx shadcn@latest init
交互式提问会依次问你:用哪个原语基座(Base UI 默认 / Radix / React Aria)、风格、基色、是否用 CSS 变量、图标库。它会生成 components.json,写出 CSS 变量主题,并装好 cn。
想跳过交互、或在 CI 里跑,直接带参数:
# 显式留在 Radix(团队已有 Radix 经验时的常见选择)
npx shadcn@latest init -b radix
# 用 preset 一键套用完整设计系统(颜色/主题/字体/图标/圆角)
npx shadcn@latest init –preset a1Dg5eFl
# 脚手架一个全新工程(CLI v4 支持 Next.js / Vite / Astro / React Router / TanStack Start / Laravel)
npx shadcn@latest init -t vite
3.3 添加组件
npx shadcn@latest add button card dialog
这条命令会:① 把 button.tsx 等文件写进 src/components/ui/;② 自动安装它需要的依赖(如 @base-ui/react 或 @radix-ui/react-slot、class-variance-authority)。从此这些文件就是你的了,随便改。
3.4 用起来
// src/App.tsx
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
export default function App() {
return (
<main className="mx-auto max-w-md p-6">
<Card>
<CardHeader>
<CardTitle>你好,shadcn/ui</CardTitle>
</CardHeader>
<CardContent className="flex flex-wrap gap-3">
<Button>默认</Button>
<Button variant="outline">描边</Button>
<Button variant="destructive" size="sm">删除</Button>
</CardContent>
</Card>
</main>
)
}
npm run dev
你应该看到一个带三种变体按钮的卡片。重点在于:variant="outline" 不是某个库预留的 prop,而是 button.tsx 里用 cva 定义出来的一行代码。 想加 variant="brand"?打开那个文件加一行即可,不需要给上游提 PR。
四、CLI v4 命令速查(2026 年最该记住的一节)
CLI 在 2026 年 3 月升到 v4,定位从「装组件的工具」变成了「面向你和你的编码 Agent 的工作台」。下面这些是真正高频的:
| shadcn init | 初始化项目 | 交互式选 base / 风格 / 基色 |
| shadcn add <组件> | 添加组件 | 核心命令,复制源码 + 装依赖 |
| shadcn add x –dry-run | 预览将要写入什么 | 不落盘,先看看 |
| shadcn add x –diff | 比对本地与上游差异 | 这是「升级」组件的正确姿势 |
| shadcn add x –view | 只看上游源码,不安装 | 当参考文档用 |
| shadcn info | 打印项目全景 | 框架/版本/CSS 变量/已装组件,喂给 Agent 特别好用 |
| shadcn docs <组件> | 从 CLI 取文档与示例 | 不用切浏览器 |
| shadcn migrate cn | 迁移到新 cn 包 | 见 5.3 节 |
| shadcn search / list / view | 检索 registry | 支持自建与 GitHub registry |
| shadcn init –monorepo | 初始化 monorepo | 多包共享一套 UI |
还有两个 registry 层面的新东西值得知道:registry:base(把整套设计系统——组件 + 依赖 + CSS 变量 + 字体——打包成一次安装)与 registry:font(字体成为一级 registry 类型,像装组件一样装字体)。
五、三大支柱:为什么它「看起来简单,用起来稳」
5.1 行为层:三个基座怎么选
这是 2026 年最需要更新认知的一块。shadcn/ui 现在可以坐在三种无头原语之上,同一套外观、同一套抽象,底层不同。
| shadcn 状态 | 2026-07 起默认 | 完全支持,未废弃 | 2026-07 起成为第三基座 |
| 初始化参数 | -b base(默认) | -b radix | –base aria |
| 组合 API | render prop | asChild | React Aria Components |
| 经验参考(gzip,社区实测) | 约 144 KB | 约 127 KB | 约 154 KB |
| 适合谁 | 新项目默认选它 | 已有 Radix 生态与经验 | 无障碍要求极高、面向多输入的团队 |
为什么换默认? 官方给的理由很务实:Base UI 由参与打造过 Radix 的原班人马做的,当时已稳定在 1.6.0、周下载 600 万+,且在 shadcn/create 上用户以约 2:1 的比例主动选了它。但官方同时明确说了两句话,值得引用:「Radix 不会被废弃」「你不需要迁移,如果你的应用跑得好,继续发布就好」。 所以「Radix 要死了」这类传言,是不成立的。
一条真正影响你代码的差异:组合方式从 Radix 的 asChild 变成了 Base UI 的 render prop。这是迁移时最容易踩的地方,也是「换个 base 不只是改个开关」的根本原因。
5.2 样式层:Tailwind + CVA
每个组件的样式都是 Tailwind 类名,变体管理交给 cva(class-variance-authority)。这带来两个好处:变体是有类型的,以及加变体不需要动上游。
// src/components/ui/button.tsx(简化示意)
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
outline: "border border-input bg-background hover:bg-accent",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
// ← 在这里加一行,你就有自己的品牌变体了
},
size: { default: "h-10 px-4 py-2", sm: "h-9 px-3", icon: "size-10" },
},
defaultVariants: { variant: "default", size: "default" },
},
)
export interface ButtonProps
extends React.ComponentProps<"button">,
VariantProps<typeof buttonVariants> {}
export function Button({ className, variant, size, …props }: ButtonProps) {
return <button className={cn(buttonVariants({ variant, size, className }))} {…props} />
}
5.3 覆盖层:cn 与 tailwind-merge(2026-09 有重要变更)
这是整个体系里最容易被忽略、但最关键的一环。问题在于:Tailwind 类不按字符串位置覆盖。组件自带 px-4,你从外面传 px-8,谁赢取决于产物 CSS 的顺序,而不是你 className 里的顺序。cn() 就是为此存在的——clsx 管条件拼接,tailwind-merge 管冲突消解,让「后传入的更具体」这个直觉成立。
2026 年 9 月这条变更是重点:过去每个项目都要在 lib/utils.ts 里复制那五行 clsx + twMerge 的包装代码;现在官方把它抽成了一个独立包 cn,是 twMerge(clsx(…)) 的 drop-in 替代,更小更快。
// lib/utils.ts —— 2026-09 起,就这一行
export { cn } from "cn"
// 你自己的组件里,用法完全没变
import { cn } from "@/lib/utils"
<div className={cn("flex items-center gap-2", isActive && "bg-primary text-primary-foreground")} />
如果是从旧项目过来,官方提供了一条迁移命令,会自动改 import、替换 twMerge(clsx(…)) 调用,并在无人引用时移除旧依赖:
npx shadcn@latest migrate cn
六、主题与深色模式:CSS 变量才是核心
shadcn/ui 的换肤不靠 JS 计算,靠 CSS 变量 + .dark 类。在 Tailwind v4 下要额外做一步「把变量映射成工具类」:
/* src/index.css */
@import "tailwindcss";
/* Tailwind v4 默认 dark: 跟随系统;这里改成跟随 .dark 类 */
@custom-variant dark (&:is(.dark *));
:root {
–background: oklch(1 0 0);
–foreground: oklch(0.145 0 0);
–primary: oklch(0.205 0 0);
–primary-foreground: oklch(0.985 0 0);
–radius: 0.625rem;
}
.dark {
–background: oklch(0.145 0 0);
–foreground: oklch(0.985 0 0);
–primary: oklch(0.922 0 0);
–primary-foreground: oklch(0.205 0 0);
}
/* 关键:@theme inline 把 CSS 变量暴露成 bg-background / text-foreground 这类工具类 */
@theme inline {
–color-background: var(–background);
–color-foreground: var(–foreground);
–color-primary: var(–primary);
–color-primary-foreground: var(–primary-foreground);
–radius-lg: var(–radius);
}
这样做的最大收益是「语义化令牌」:组件里写的是 bg-background text-foreground,而不是 bg-white text-gray-900。于是深色模式几乎免费——切一个 <html class="dark">,所有组件自动换色;换品牌色也只改 –primary 一处。配合 next-themes 的 attribute="class" 即可实现持久化的亮暗切换。
七、新手必踩的七个坑
| 没配 @ 路径别名 | init 报找不到目录;组件里 @/lib/utils 全红 | vite 的 resolve.alias 与 tsconfig 的 paths 三处保持一致 |
| Tailwind 没跑通就 init | 组件加进来但毫无样式 | 先确认 @import "tailwindcss" + @tailwindcss/vite 生效 |
| 以为升 npm 版本就是升级组件 | 上游修的 bug 一直没进来 | 组件是源码,用 shadcn add x –diff 比对后手动 merge |
| 在 @theme 外改变量 | 新变量不生成对应工具类 | Tailwind v4 必须在 @theme / @theme inline 里做 –color-* 映射 |
| 深色模式切了没反应 | 加 .dark 无效 | 需要 @custom-variant dark (&:is(.dark *));,并确保类挂在 <html> 上 |
| 用样式表硬刚组件样式 | 加了 !important 还是不稳 | 通过 cn() 从 props 传 class,走 tailwind-merge 的正规覆盖路径 |
| 把 components/ui 当业务目录 | 越写越乱,且无法再和上游比对 | ui/ 只放原语组件;业务组件另建目录基于 ui/ 组合 |
最后一条值得展开:components/ui/ 应该保持「薄」。你改得越多,将来 –diff 合并上游修复就越痛。业务逻辑放外面,ui/ 只做视觉与交互原语——这是 shadcn 项目能长期演进的关键约定。
八、选型实战:shadcn/ui 到底该不该用
8.1 四方案正面对比
| 交付形态 | CLI 复制源码进你的仓库 | npm 依赖 | npm 依赖 | npm 依赖(仅行为) |
| 依赖体积 | 仅原语 + Tailwind + CVA(社区经验值:按用量约 20–50 KB 增量) | 较大 | 较大 | 最小 |
| 定制成本 | 低(直接改源码) | 高(打 API / 主题 / sx) | 中高 | 低但样式全自理 |
| 视觉自带程度 | 中性,无强品牌感 | Material 味重 | 中后台感强 | 零 |
| 无障碍 | 原生承担(Base UI / Radix / React Aria) | 好 | 好 | 同左,但交互要自己接 |
| 升级路径 | 手动 diff 合并(无自动升级) | 改版本号 | 改版本号 | 改版本号 |
| 学习成本 | 中(要懂 Tailwind + cva + cn) | 中 | 中 | 高(要自己搭样式体系) |
| 适合场景 | 用 Tailwind 的 React 应用、SaaS 后台、需要深度定制的产品 | 快速交付、要 MUI X 数据网格 | 中后台、团队熟 AntD | 已有设计体系、只要行为层 |
一句话判据:你是在「买一套成品 UI」,还是在「攒一套自己的 UI」? 买成品选 MUI / AntD,攒自己的选 shadcn + 无头原语。这不是谁更先进的问题,是两类需求。
8.2 决策树
#mermaid-svg-ID1FTxpbkK0xb3NG{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ID1FTxpbkK0xb3NG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ID1FTxpbkK0xb3NG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ID1FTxpbkK0xb3NG .error-icon{fill:#552222;}#mermaid-svg-ID1FTxpbkK0xb3NG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ID1FTxpbkK0xb3NG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ID1FTxpbkK0xb3NG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ID1FTxpbkK0xb3NG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ID1FTxpbkK0xb3NG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ID1FTxpbkK0xb3NG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ID1FTxpbkK0xb3NG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ID1FTxpbkK0xb3NG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ID1FTxpbkK0xb3NG .marker.cross{stroke:#333333;}#mermaid-svg-ID1FTxpbkK0xb3NG svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ID1FTxpbkK0xb3NG p{margin:0;}#mermaid-svg-ID1FTxpbkK0xb3NG .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-ID1FTxpbkK0xb3NG .cluster-label text{fill:#333;}#mermaid-svg-ID1FTxpbkK0xb3NG .cluster-label span{color:#333;}#mermaid-svg-ID1FTxpbkK0xb3NG .cluster-label span p{background-color:transparent;}#mermaid-svg-ID1FTxpbkK0xb3NG .label text,#mermaid-svg-ID1FTxpbkK0xb3NG span{fill:#333;color:#333;}#mermaid-svg-ID1FTxpbkK0xb3NG .node rect,#mermaid-svg-ID1FTxpbkK0xb3NG .node circle,#mermaid-svg-ID1FTxpbkK0xb3NG .node ellipse,#mermaid-svg-ID1FTxpbkK0xb3NG .node polygon,#mermaid-svg-ID1FTxpbkK0xb3NG .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ID1FTxpbkK0xb3NG .rough-node .label text,#mermaid-svg-ID1FTxpbkK0xb3NG .node .label text,#mermaid-svg-ID1FTxpbkK0xb3NG .image-shape .label,#mermaid-svg-ID1FTxpbkK0xb3NG .icon-shape .label{text-anchor:middle;}#mermaid-svg-ID1FTxpbkK0xb3NG .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ID1FTxpbkK0xb3NG .rough-node .label,#mermaid-svg-ID1FTxpbkK0xb3NG .node .label,#mermaid-svg-ID1FTxpbkK0xb3NG .image-shape .label,#mermaid-svg-ID1FTxpbkK0xb3NG .icon-shape .label{text-align:center;}#mermaid-svg-ID1FTxpbkK0xb3NG .node.clickable{cursor:pointer;}#mermaid-svg-ID1FTxpbkK0xb3NG .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ID1FTxpbkK0xb3NG .arrowheadPath{fill:#333333;}#mermaid-svg-ID1FTxpbkK0xb3NG .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ID1FTxpbkK0xb3NG .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ID1FTxpbkK0xb3NG .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ID1FTxpbkK0xb3NG .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ID1FTxpbkK0xb3NG .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ID1FTxpbkK0xb3NG .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ID1FTxpbkK0xb3NG .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ID1FTxpbkK0xb3NG .cluster text{fill:#333;}#mermaid-svg-ID1FTxpbkK0xb3NG .cluster span{color:#333;}#mermaid-svg-ID1FTxpbkK0xb3NG div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ID1FTxpbkK0xb3NG .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ID1FTxpbkK0xb3NG rect.text{fill:none;stroke-width:0;}#mermaid-svg-ID1FTxpbkK0xb3NG .icon-shape,#mermaid-svg-ID1FTxpbkK0xb3NG .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ID1FTxpbkK0xb3NG .icon-shape p,#mermaid-svg-ID1FTxpbkK0xb3NG .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ID1FTxpbkK0xb3NG .icon-shape .label rect,#mermaid-svg-ID1FTxpbkK0xb3NG .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ID1FTxpbkK0xb3NG .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ID1FTxpbkK0xb3NG .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ID1FTxpbkK0xb3NG :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
不用
不愿意
愿意
用
不需要, 只要几个静态组件
需要
不要, 只要开箱即用
要
不太需要
需要
项目用 Tailwind 吗
愿意为它引入吗
用 MUI / Ant Design或裸用无头原语自配样式
需要复杂的可访问交互吗
几行 Tailwind 手写就够不必上 shadcn
要完全掌控组件源码吗
MUI / Ant Design
shadcn/ui
需要持续跟随上游修复吗
复制即冻结, 直接改最省心
建立 diff 合并流程或自建团队 registry
8.3 我的结论:分层使用,而不是全盘接管
shadcn/ui 最容易被误用的方式,是把它当成「又一个必须全盘采用的组件库」。 它其实是一个可以按层取舍的体系:
- 原语层:Base UI / Radix / React Aria 承担真正难的部分(焦点管理、ARIA、键盘导航)。不要重造这一层,这是它最有价值的赠品。
- 原语组件层(components/ui/):保持薄,尽量少改。改动越少,将来合并上游越轻松。
- 业务组件层:基于 ui/ 组合与封装的领域组件放这里,可以有大量自己的逻辑与样式。
- 令牌层:所有颜色、圆角、字体走 CSS 变量语义令牌。换肤只改这一层,这是设计系统真正的单一数据源。
- 分发层:团队规模化后,把业务组件层发布成自己的 registry,用 shadcn add acme/xxx 一键投递。
换句话说:享受它的原语与令牌体系,但把「改了多少源码」当成一项要主动管理的技术债。 这比「要么全用要么不用」务实得多。
九、FAQ
Q1:Radix 要被废弃了吗?我必须迁移吗? 不用。官方明确表态:Radix 未被废弃,每个更新与新组件都会同时为两套原语发布(除非某组件只在 Base UI 存在)。原文的态度是「如果你的应用跑得好,继续发布就好」。新项目想留在 Radix,加 -b radix 即可——如果你的 CI 里有非交互式 shadcn init,务必补上这个参数,否则它会默认切到 Base UI。
Q2:从 Radix 迁到 Base UI 难吗? 官方没做 codemod,理由很直接:你已经改过你的副本了,codemod 恰好会在这类文件上出错。 替代方案是官方提供的 Agent skill(一条命令安装,然后让它「migrate accordion to base-ui」),逐个组件渐进迁移,每个组件一个 commit、一份变更报告,中途停下来也不影响可发布。官方实测:60+ 组件(其中 36 个在 Radix)约 25 分钟完成,每个组件约 1 万 token。
Q3:必须是 Next.js 吗?Vue 能用吗? 不是必须。CLI 的 -t 模板覆盖 Next.js / Vite / React Router / Astro / TanStack Start / Laravel。但官方主线是 React:Vue、Svelte、Solid 是社区移植项目(shadcn-vue、shadcn-svelte 等),令牌体系与设计思路一致,但组件不是同一份代码,不要指望跨框架复用。
Q4:体积会不会很大? 按社区实测经验值,典型项目按实际使用组件数增加约 20–50 KB(含 gzip),比整体引入 MUI / AntD 轻,但不要和「裸用原语」比——单看某个基座的产物,社区实测 Radix 版约 127 KB、Base UI 版约 144 KB(gzip,样式与组件集相同的场景)。真正的取舍不在大小,在耦合方式。
Q5:上游更新了组件,我怎么跟上? 标准流程是三步:–view 看上游现在长什么样 → –diff 看你本地差在哪 → 手动 merge 你真正想要的部分。不要盲目 add 覆盖,那会抹掉你的定制。如果团队规模大,建议自建 registry 锁定版本与约定。
Q6:lib/utils.ts 里的 cn 那五行怎么没了? 2026 年 9 月起官方把 cn 抽成了独立包,lib/utils.ts 只剩 export { cn } from "cn" 一行。旧项目跑 npx shadcn@latest migrate cn 即可平滑迁移,不迁移也不会坏。
十、总结
shadcn/ui 最深刻的地方不是那些好看的组件,而是它提出的一个前提:UI 代码本来就该属于你。 一旦接受这个前提,你会发现它不需要被「选型」,它只是把一件本来就该如此的事,做得顺手了一点。
参考与延伸阅读
- shadcn/ui 官方文档与 Changelog(ui.shadcn.com/docs/changelog):July 2026 – Base UI as the Default、July 2026 – React Aria、March 2026 – shadcn/cli v4、August 2026 – Private GitHub Registries、September 2026 – cn
- shadcn/ui 官方 CLI 文档(init / add / –dry-run / –diff / –view / migrate / info / docs)
- shadcn/ui 官方 Registry 文档(registry:base、registry:font、GitHub registry、Dynamic Search)
- Base UI 官方文档(base-ui.com)、Radix Primitives 文档(radix-ui.com)、React Aria 文档(react-spectrum.adobe.com/react-aria)
- 相关依赖:class-variance-authority、cn 包、tailwind-merge、clsx
- 对照方案:MUI(含 MUI X 数据网格)、Ant Design、Mantine、Park UI、Ark UI、Headless UI
- 延伸话题:WAI-ARIA Authoring Practices、CSS 变量语义化令牌、Tailwind v4 的 @theme inline
声明:本文为技术分析与选型建议,涉及具体版本号、默认值、体积与迁移耗时,请以官方文档与你自己的实测为准;文中标注「经验值 / 社区实测 / 行业参考」的数字来自公开讨论与第三方基准测试,仅作量级感知,非厂商基准。文中代码为简化演示,未做工程加固(如完整无障碍实现、错误处理、asChild/render 的全部边界)。
如果这篇对你有帮助,欢迎点赞 + 收藏 + 关注,也欢迎关注我的专栏获取后续更新。



