shadcn/ui
📒 shadcn/ui 的作用和使用方式。
shadcn/ui 不是传统意义上的组件库。
它更像一套“组件代码生成工具”:通过 CLI 把 Button、Dialog、Input 这些组件代码添加到你的项目里。添加完成后,组件文件就在你的项目源码中,可以直接修改。
它解决什么问题
做 React / Next.js 项目时,经常会重复写这些基础 UI:
- Button
- Input
- Dialog
- Dropdown
- Tabs
- Form
如果完全自己写,样式、交互、无障碍细节都要处理;如果使用传统组件库,组件又常常被封装在依赖包里,不太好深度修改。
shadcn/ui 的思路是:直接把一份结构清晰、默认好看的组件源码放进项目,让你既能快速开始,也能自由修改。
它的工作方式
shadcn/ui 的核心不是运行时组件库,而是 CLI。
CLI 主要负责:
- 初始化项目配置。
- 生成
components.json。 - 添加组件源码。
- 安装组件需要的依赖。
比如执行:
npx shadcn@latest init会生成类似:
components.json再执行:
npx shadcn@latest add button会生成类似:
src/components/ui/button.tsx之后你在项目里使用的是本地文件:
import { Button } from "@/components/ui/button";也就是说,运行时并不是从 shadcn/ui 这个包里 import Button,而是从你项目自己的 components/ui/button.tsx 里 import。
运行时依赖
生成后的组件代码会用到一些实际依赖。
常见依赖包括:
@radix-ui/react-slot:让组件可以把样式和行为传给子元素,常用于asChild。class-variance-authority:管理组件变体,比如按钮的大小、颜色、样式类型。clsx:按条件拼接 className。tailwind-merge:合并 Tailwind class,避免冲突。lucide-react:常用图标。
所以可以这样理解:
shadcn CLI:负责生成和添加代码。components.json:记录组件生成配置。src/components/ui/*:真正存在项目里的组件源码。@radix-ui/*、class-variance-authority、clsx等:组件运行时真正用到的依赖。
和普通组件库的区别
普通组件库通常这样用:
import { Button } from "some-ui-library";组件源码在依赖包里,你主要通过 props、主题配置或覆盖样式来调整。
shadcn/ui 通常这样用:
import { Button } from "@/components/ui/button";组件源码就在项目里。你可以直接改:
- JSX 结构。
- Tailwind class。
- 组件 props。
- 变体配置。
- 默认交互。
这也是它最重要的特点:不是安装一套黑盒组件,而是把可维护的组件源码放进项目。
基本使用方式
初始化:
npx shadcn@latest init按需添加组件:
npx shadcn@latest add button
npx shadcn@latest add dialog
npx shadcn@latest add input使用组件:
import { Button } from "@/components/ui/button";
export function SaveButton() {
return <Button>保存</Button>;
}Demo:添加 Dialog 组件
以 Dialog 弹窗为例,看一下 shadcn/ui 生成组件代码的过程。
1. 执行命令
npx shadcn@latest add dialog这条命令会做几件事:
- 检查
components.json配置。 - 安装 Dialog 需要的依赖。
- 生成 Dialog 组件源码。
- 把组件放到你的
components/ui目录里。
常见生成文件:
src/components/ui/dialog.tsx常见新增依赖:
@radix-ui/react-dialogDialog 组件内部通常还会用到项目里的工具函数,比如:
import { cn } from "@/lib/utils";cn 一般是 clsx + tailwind-merge 的封装,用来合并 className。
2. 生成后的组件是什么
生成的 dialog.tsx 大概会导出这些组件:
Dialog
DialogTrigger
DialogContent
DialogHeader
DialogTitle
DialogDescription
DialogFooter
DialogClose这些组件不是从 shadcn/ui 运行时加载的,而是你项目里的本地源码。
你使用时是这样导入:
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";3. 在页面中使用
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
export function DeleteUserDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">删除用户</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>确认删除用户?</DialogTitle>
<DialogDescription>
删除后数据将无法恢复,请谨慎操作。
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
);
}这里发生了几件事:
Dialog:管理弹窗打开和关闭状态。DialogTrigger:触发打开弹窗。asChild:让Button作为真正的触发元素。DialogContent:弹窗主体。DialogTitle/DialogDescription:提供标题、描述和无障碍语义。
4. 为什么它依赖 Radix UI
Dialog 看起来只是一个弹窗,但实际包含很多交互细节:
- 打开后焦点进入弹窗。
- 按
Esc可以关闭。 - 点击遮罩可以关闭。
- 关闭后焦点回到触发按钮。
- 屏幕阅读器能识别标题和描述。
这些底层行为通常由 @radix-ui/react-dialog 处理。
shadcn/ui 生成的 dialog.tsx 主要做的是:
- 引入 Radix Dialog 原语。
- 包一层项目自己的组件命名。
- 加上 Tailwind CSS 样式。
- 暴露更适合项目使用的组件。
5. 如何修改样式
因为 dialog.tsx 已经在你的项目里,所以可以直接打开文件修改。
比如你想修改弹窗宽度,可以找到 DialogContent 里的 className:
"sm:max-w-lg"改成:
"sm:max-w-xl"这就是 shadcn/ui 和传统组件库最大的区别:组件源码属于你的项目,不是依赖包里的黑盒。
适合什么场景
适合:
- React / Next.js 项目。
- 使用 Tailwind CSS 的项目。
- 希望快速拥有一套基础 UI。
- 希望组件源码可以直接修改。
- 希望和 Radix UI、lucide-react 等生态搭配使用。
不太适合:
- 非 React 项目。
- 不使用 Tailwind CSS 的项目。
- 希望通过升级依赖自动更新所有组件的场景。
使用建议
不要一次性添加所有组件。
建议需要什么加什么:
npx shadcn@latest add button项目里可以约定:
components/ui:放 shadcn/ui 生成的基础组件。components/common:放业务无关的通用组件。components/feature:放具体业务模块组件。
这样基础 UI 和业务组件会更清楚。
总结
shadcn/ui 的重点是“生成组件源码”,不是“提供运行时组件库”。CLI 负责把代码添加到项目,真正运行的是项目里的 src/components/ui/* 文件,以及这些组件用到的 Radix UI、CVA、clsx、tailwind-merge、lucide-react 等依赖。