起因
要给 React 项目快速搭出"看着像样"的 UI。选择:
- Material UI / Chakra:大、深度定制难、bundle 大
- Ant Design:大且企业风、设计语言强
- Headless UI / Radix:无样式、要自己写 CSS
shadcn/ui 是 2023 后流行的新范式:不是 npm 包,而是 "复制粘贴
组件源码进你的项目"。底层用 Radix Primitives 做 a11y / 行为,
Tailwind 做样式。你拥有源码,随便改。
解决方案
装
前提:React + Tailwind CSS 项目。
npx shadcn-ui@latest init
# 交互式问几个问题:
# Style: default / new-york
# Base color: slate / gray / zinc / ...
# CSS variables: yes
生成 components.json 配置。
加组件
npx shadcn-ui@latest add button
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add toast
每个组件作为 .tsx 文件被复制到 src/components/ui/:
src/components/ui/
├── button.tsx
├── dialog.tsx
├── toast.tsx
└── ...
打开 button.tsx:
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
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-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: { variant: "default", size: "default" },
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }
这是你的代码。要加一个 success variant 直接改就行:
variant: {
default: "...",
success: "bg-green-600 text-white hover:bg-green-700",
...
},
不像传统组件库要 fork 整个 repo 或者写 wrap。
用
import { Button } from '@/components/ui/button'
<Button>Click me</Button>
<Button variant="outline" size="lg">Outline</Button>
<Button variant="success">Saved</Button>
Dialog 例
import { Dialog, DialogContent, DialogHeader, DialogTitle,
DialogTrigger, DialogFooter } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive">删除</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>确认删除</DialogTitle>
</DialogHeader>
<p>真的要删除这条记录吗?</p>
<DialogFooter>
<Button variant="outline">取消</Button>
<Button variant="destructive">确认</Button>
</DialogFooter>
</DialogContent>
</Dialog>
底层 Radix Dialog 已经处理好:focus trap、ESC 关闭、aria-modal、
overlay 点击关闭、滚动锁等所有 a11y 细节。
配色:CSS variables
globals.css:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
/* ... */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... */
}
}
改这些变量 → 整个组件库换色。dark mode <html class="dark"> 自动切。
theming:从 ui.shadcn.com/themes 抄
shadcn 官网有 themes 页面,挑色调 + 复制 CSS 变量 → 替换你的 globals.css。
3 秒换主题。
组件清单
shadcn 现有 50+ 组件:
- Button / Input / Label / Textarea / Select / Switch / Checkbox / Radio
- Dialog / Sheet / Popover / Tooltip / HoverCard / ContextMenu /
DropdownMenu / Menubar / NavigationMenu / Command (cmd-k palette) - Card / Badge / Avatar / Separator / Skeleton / Progress
- Toast / Alert / AlertDialog
- Table / DataTable
- Tabs / Accordion / Collapsible
- Calendar / DatePicker
- Form (react-hook-form 集成)
- Carousel / Resizable / Drawer
涵盖 95% web app 需求。
数据表 + sorting + filter(DataTable)
import { useReactTable } from '@tanstack/react-table'
// shadcn 用 TanStack Table 做底层
const table = useReactTable({
data: users,
columns,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
})
// 渲染部分 shadcn 提供的 Table 组件包
复杂表格 < 100 行 + 完全可定制。
与传统组件库对比
| shadcn/ui | MUI v5 | Ant Design | |
|---|---|---|---|
| 引入 | 复制源码 | npm i | npm i |
| 改样式 | 直接改 .tsx | sx prop / theme override | 复杂 token |
| Bundle | 仅用到的组件 | 大(tree-shaking 不彻底) | 极大 |
| Tailwind | ✅ 原生 | 需适配 | 需适配 |
| 升级 | 自己控制 | 升级有 breaking risk | 升级有 breaking risk |
| 设计风格 | 默认 minimal | Material | 企业风 |
shadcn 的"复制源码"哲学是关键差异:
- 优点:完全掌控、零依赖、bundle 最小、修改简单
- 缺点:组件库更新不会自动应用(要重新
add覆盖)
效果
我们一个内部工具用 shadcn 重做,对比之前的 Material UI 版:
- bundle 从 850 KB → 280 KB
- 设计师对样式调整一句话就能改(不需要"懂 MUI theme")
- 暗色模式切换 0 配置
- 新人 onboarding 时间减半(组件就在自己代码里,IDE 跳定义直接看)
与同生态组合
- next-themes:dark mode 切换
- react-hook-form + zod:表单(shadcn Form 组件直接集成)
- TanStack Query:数据
- TanStack Table:表格
- lucide-react:icon
- sonner:toast 替代品(更现代)
一套组合下来基本 cover 现代 React app 全部需求。
踩过的坑
-
第一次 init 必须有 Tailwind:shadcn 强依赖 Tailwind utility classes。
非 Tailwind 项目要么先装 Tailwind,要么用 tremor
等替代。 -
npx shadcn-ui add覆盖修改:再次 add 同名组件会覆盖你改过的
版本。要么不再 add(自己维护),要么 git 改动检查。 -
asChild模式坑:<Button asChild><a href="/">link</a></Button>
把样式应用到 child 上。新人不知道这个 pattern 时疑惑。
Radix Slot 的设计,多看几次就熟。 -
CSS 变量值是 HSL 三段值不是颜色字符串:
--primary: 220 90% 56%
不是--primary: hsl(220, 90%, 56%)。tailwind config 用
hsl(var(--primary) / <alpha-value>)拼。 -
monorepo 共享 components/ui:复制到每个 app 都改 = 重复劳动。
抽到packages/ui共享,但 shadcn 自动 add 路径不知道。
手动维护 import 路径或写自己的 CLI wrapper。
登录后参与评论。