1.1 它到底是什么
第一次接触的人常问:"shadcn/ui 的 npm 包叫什么?" 答案是:没有。它不是一个你 import 的组件库。它是一份组件源码集合 + 一个 CLI 工具。你运行命令,它把某个组件的 .tsx 源文件复制进你的项目,从此这份代码就属于你。
作者 shadcn(Vercel 员工 hunter)称之为 "open code"(开放代码) 分发模式。传统库是"依赖",shadcn/ui 是"脚手架"——把最佳实践的组件实现直接播种到你的仓库里。
- 组件库(Component Library)
- 如 MUI、Ant Design——你
npm install后import使用,组件实现藏在 node_modules,你只能通过它给的 props/theme API 定制。 - Headless UI / 无头组件
- 如 Radix UI、Headless UI、React Aria——只提供行为与可访问性逻辑(无样式),样式由你自己写。shadcn/ui 的底座就是 Radix。
- Radix UI Primitives
- Radix 出品的一套无样式、无障碍的 React 原语(Dialog、Popover、Select…),处理焦点管理、键盘导航、ARIA、Portal。shadcn/ui 在其上包一层 Tailwind 样式。
- Tailwind CSS
- 原子化 CSS 框架,用
class="px-4 py-2 rounded-md"直接在标签上写样式。shadcn/ui 所有视觉都用 Tailwind class 表达。 - Registry(注册表)
- CLI 拉取组件的来源,一个描述组件文件/依赖的 JSON 端点。官方有默认 registry,你也能自建分发自己的组件。
- cn() 工具函数
- shadcn 项目里几乎每个组件都用到的合并类名助手,基于
clsx+tailwind-merge,解决条件类名与 Tailwind 冲突去重。 - class-variance-authority (cva)
- 声明式定义组件"变体"(variant/size)到 Tailwind class 的映射的库,Button 的
variant="destructive"就靠它。 - components.json
- 项目根目录的配置文件,记录你的风格、别名、Tailwind 路径等,CLI 每次
add都读它。
1.2 "拥有代码"意味着什么
这不是营销话术,而是有实际后果的架构决策:
- 随意修改:想给 Button 加一个
loading态?直接改components/ui/button.tsx,不用等上游合并 PR、不用写包裹层对抗它的样式。 - 无版本锁定:组件代码在你仓库里,库作者发新版本不会突然破坏你的 UI。你按需 re-run CLI 来"升级"某个组件(本质是覆盖文件)。
- 无黑盒:调试时组件代码就在源码里,断点直接打进去,不用翻 node_modules 里编译后的产物。
- 可读性即文档:想知道 Dialog 怎么实现的?打开文件读 Radix 用法即可。
把 shadcn/ui 当成"一本你可以直接抄进项目的组件食谱",而不是"一个要小心供养的依赖"。抄进来后,它就是你的代码,享受一切修改自由,也承担维护责任。
1.3 vs MUI / Ant Design / Chakra
| 维度 | shadcn/ui | MUI | Ant Design | Chakra UI |
|---|---|---|---|---|
| 分发方式 | 复制源码进项目 | npm 依赖 | npm 依赖 | npm 依赖 |
| 样式方案 | Tailwind + CSS 变量 | Emotion(CSS-in-JS) | Less / CSS-in-JS | Emotion |
| 可访问性底座 | Radix UI | 自研 | 自研 | 自研 / Ark |
| 定制方式 | 改源码,随便改 | theme + sx + styled | ConfigProvider + token | theme 扩展 |
| 包体积影响 | 只含你 add 的组件 | 较大 | 较大 | 中等 |
| 升级模型 | 按需重新拉组件 | bump 版本 | bump 版本 | bump 版本 |
| 设计风格 | 极简中性,易改 | Material Design | 企业中后台 | 友好圆润 |
| 框架耦合 | React(Vue/Svelte 有社区移植) | React | React | React |
• React + Tailwind + 想完全掌控样式 → shadcn/ui
• 想开箱即用一整套 Material 设计 → MUI
• 做企业中后台、要海量现成业务组件 → Ant Design
• 喜欢 props 式样式、不想上 Tailwind → Chakra
1.4 前置依赖
shadcn/ui 假设你已经有一个 React + Tailwind 项目。它本身不引入运行时依赖,但组件会用到几个小库:
- React 18+ 与一个框架(Next.js / Vite / Remix / Astro 均可)
- Tailwind CSS(v3 或 v4,CLI 会适配)
- TypeScript(强烈推荐,组件都是
.tsx) - 组件按需拉取的:
@radix-ui/*、class-variance-authority、clsx、tailwind-merge、lucide-react(图标)
1.5 第一次 init
在已有的 React + Tailwind 项目根目录运行:
# 初始化 shadcn/ui:生成 components.json、写入 CSS 变量、装工具依赖
$ npx shadcn@latest init
# CLI 会交互式提问(也可用 flag 跳过):
# Which style? → new-york(推荐)/ default
# Base color? → zinc / slate / stone / gray / neutral
# CSS variables for colors? → yes
init 完成后你会看到项目里多了/改了这些:
# 新增/修改的文件
components.json # shadcn 配置
src/lib/utils.ts # 里面导出 cn() 函数
src/index.css # 注入了 :root / .dark 的 CSS 变量
tailwind.config.ts # v3 时会映射颜色变量(v4 用 @theme)
1.6 添加你的第一个组件
# 拉取 Button 组件源码进项目
$ npx shadcn@latest add button
# 一次添加多个
$ npx shadcn@latest add card input label
执行后,src/components/ui/button.tsx 就出现了。它不是从 node_modules 引用,而是真实存在于你的仓库。用起来:
// app/page.tsx
import { Button } from "@/components/ui/button";
export default function Home() {
return (
<div className="p-8 space-x-2">
<Button>默认按钮</Button>
<Button variant="destructive">危险</Button>
<Button variant="outline">描边</Button>
</div>
);
}
上面的 @/components/ui/button 依赖 TS/构建工具配置了 @ 指向 src 的别名。若你还没配,init 时 CLI 会引导,或在 tsconfig.json 的 paths 里补 "@/*": ["./src/*"]。下一章详解。
1.7 一眼看懂 button.tsx
拉进来的 Button 大致长这样(简化)——这就是"你拥有的代码":
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",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-white hover:bg-destructive/90",
outline: "border bg-background hover:bg-accent",
},
size: { default: "h-9 px-4 py-2", sm: "h-8 px-3", lg: "h-10 px-6" },
},
defaultVariants: { variant: "default", size: "default" },
}
);
function Button({ className, variant, size, ...props }) {
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />;
}
export { Button, buttonVariants };
关键点:cva 定义变体到 class 的映射,cn 合并外部传入的 className。想加变体?直接在这个文件里加一行 variant——它是你的代码。
1.8 小结与下一步
- shadcn/ui 不是组件库,而是"复制源码 + CLI"的开放代码分发模式,代码归你所有。
- 底座是 Radix UI(可访问性行为)+ Tailwind(样式)+ cva/cn(变体与类名合并)。
- 相比 MUI/Ant/Chakra,它牺牲"开箱即用"换来"完全掌控",最适合 React + Tailwind 技术栈。
- 下一章深入安装与配置——把
components.json、Tailwind、路径别名、CSS 变量、暗色模式一次配到位。