Chapter 01

认识 shadcn/ui

理解"复制源码、代码归你所有"这一颠覆性理念,看清它在 MUI/Ant/Chakra 格局中的独特定位,并跑通第一次 init。

1.1 它到底是什么

第一次接触的人常问:"shadcn/ui 的 npm 包叫什么?" 答案是:没有。它不是一个你 import 的组件库。它是一份组件源码集合 + 一个 CLI 工具。你运行命令,它把某个组件的 .tsx 源文件复制进你的项目,从此这份代码就属于你。

作者 shadcn(Vercel 员工 hunter)称之为 "open code"(开放代码) 分发模式。传统库是"依赖",shadcn/ui 是"脚手架"——把最佳实践的组件实现直接播种到你的仓库里。

组件库(Component Library)
如 MUI、Ant Design——你 npm installimport 使用,组件实现藏在 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 "拥有代码"意味着什么

这不是营销话术,而是有实际后果的架构决策:

心智转变

把 shadcn/ui 当成"一本你可以直接抄进项目的组件食谱",而不是"一个要小心供养的依赖"。抄进来后,它就是你的代码,享受一切修改自由,也承担维护责任。

1.3 vs MUI / Ant Design / Chakra

维度shadcn/uiMUIAnt DesignChakra UI
分发方式复制源码进项目npm 依赖npm 依赖npm 依赖
样式方案Tailwind + CSS 变量Emotion(CSS-in-JS)Less / CSS-in-JSEmotion
可访问性底座Radix UI自研自研自研 / Ark
定制方式改源码,随便改theme + sx + styledConfigProvider + tokentheme 扩展
包体积影响只含你 add 的组件较大较大中等
升级模型按需重新拉组件bump 版本bump 版本bump 版本
设计风格极简中性,易改Material Design企业中后台友好圆润
框架耦合React(Vue/Svelte 有社区移植)ReactReactReact
如何选

• React + Tailwind + 想完全掌控样式 → shadcn/ui
• 想开箱即用一整套 Material 设计 → MUI
• 做企业中后台、要海量现成业务组件 → Ant Design
• 喜欢 props 式样式、不想上 Tailwind → Chakra

1.4 前置依赖

shadcn/ui 假设你已经有一个 React + Tailwind 项目。它本身不引入运行时依赖,但组件会用到几个小库:

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.jsonpaths 里补 "@/*": ["./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 小结与下一步