shadcn/ui 改变了开发者对 React 组件库的认知。它不是传统的 npm 包,而是让你将精心设计的无障碍组件直接复制到项目中。你拥有代码所有权,可以自定义一切,没有依赖锁定。基于 Radix UI 原语和 Tailwind CSS 构建,shadcn/ui 已成为 2026 年构建生产级 React 界面的首选。
shadcn/ui 是一组可复用的无障碍 React 组件,你将它们复制到项目中而非作为依赖安装。基于 Radix UI 和 Tailwind CSS,完全掌控组件代码。使用 CLI 添加组件,通过 CSS 变量自定义主题,配合 React Hook Form + Zod 构建表单,配合 TanStack Table 构建数据表格。
- shadcn/ui 不是组件库 — 它是你拥有的复制粘贴组件集合
- 基于 Radix UI 原语实现无障碍,使用 Tailwind CSS 进行样式设计
- 使用 CLI(npx shadcn@latest add)向项目添加组件
- 通过 CSS 变量实现主题 — 开箱支持暗黑模式
- 配合 React Hook Form + Zod 实现类型安全的表单验证
- TanStack Table 集成实现强大灵活的数据表格
- components.json 配置路径、样式和组件别名
- 完全自定义 — 因为你拥有源代码,可以修改任何组件
什么是 shadcn/ui?
shadcn/ui 不是传统的组件库。你不需要在 package.json 中安装它。它提供 CLI 和组件注册表,你可以将组件直接复制到项目中,拥有每一行代码。
每个组件基于 Radix UI 构建 — 一个无头的、无样式的无障碍组件库 — 并用 Tailwind CSS 设计样式。
核心理念是:用你需要的,改你想改的。没有主题 API、没有包装组件、没有库更新带来的破坏性变更。
# The shadcn/ui approach:
# 1. You run the CLI to add a component
npx shadcn@latest add button
# 2. The component is copied into your project
# src/components/ui/button.tsx is created
# 3. You import and use it like any local component
import { Button } from "@/components/ui/button"
# 4. You own the code - edit it however you want
# No library dependency, no version conflicts安装和设置
shadcn/ui 支持多种 React 框架。CLI 处理初始设置,包括创建 components.json 配置文件、设置 Tailwind CSS 和创建工具目录。
Next.js 安装
# Create a new Next.js project
npx create-next-app@latest my-app --typescript --tailwind --eslint
cd my-app
# Initialize shadcn/ui
npx shadcn@latest init
# The CLI will ask you:
# - Which style? (default / new-york)
# - Which color? (slate / gray / zinc / neutral / stone)
# - Use CSS variables for colors? (yes)
# Add your first component
npx shadcn@latest add button
# Add multiple components at once
npx shadcn@latest add button card dialog input labelVite 安装
# Create a Vite + React + TypeScript project
npm create vite@latest my-app -- --template react-ts
cd my-app
# Install Tailwind CSS
npm install -D tailwindcss @tailwindcss/vite
# Update vite.config.ts to include the plugin
# import tailwindcss from "@tailwindcss/vite"
# plugins: [react(), tailwindcss()]
# Configure path aliases in tsconfig.json
# "paths": { "@/*": ["./src/*"] }
# Initialize shadcn/ui
npx shadcn@latest initRemix 安装
# Create a Remix project with Vite
npx create-remix@latest my-app
cd my-app
# Install and configure Tailwind CSS
npm install -D tailwindcss @tailwindcss/vite
# Initialize shadcn/ui
npx shadcn@latest init
# Add components as needed
npx shadcn@latest add button dialog form理解 components.json
components.json 是项目中 shadcn/ui 的核心配置文件。它告诉 CLI 在哪里放置组件、如何处理样式以及使用什么导入别名。
// components.json
{
"\$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "zinc",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}关键字段:style 选择视觉风格,tailwind.config 指向 Tailwind 配置,tailwind.css 指向全局 CSS,aliases 映射导入路径。
使用组件
添加组件只需运行 CLI 命令。组件文件会被复制到 components.json 中指定的路径。
Button 按钮组件
Button 组件支持多种变体(default、destructive、outline、secondary、ghost、link)和尺寸(default、sm、lg、icon)。
// Install: npx shadcn@latest add button
import { Button } from "@/components/ui/button"
export function ButtonExamples() {
return (
<div>
{/* Variants */}
<Button variant="default">Default</Button>
<Button variant="destructive">Delete</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
{/* Sizes */}
<Button size="sm">Small</Button>
<Button size="default">Default</Button>
<Button size="lg">Large</Button>
<Button size="icon"><IconComponent /></Button>
{/* With loading state */}
<Button disabled>
<Loader2 style={{ animation: "spin 1s linear infinite" }} />
Please wait
</Button>
</div>
)
}Dialog 对话框组件
Dialog 组件基于 Radix UI Dialog 构建,提供无障碍的模态对话框,处理焦点陷阱、键盘导航和屏幕阅读器播报。
// Install: npx shadcn@latest add dialog
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
export function DialogExample() {
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Edit Profile</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit profile</DialogTitle>
<DialogDescription>
Make changes to your profile here.
</DialogDescription>
</DialogHeader>
<div>
<Label htmlFor="name">Name</Label>
<Input id="name" defaultValue="John Doe" />
</div>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}Command 命令面板组件
Command 组件基于 cmdk 构建,提供可搜索的命令面板界面,支持键盘导航 — 适用于命令菜单、搜索对话框和组合框。
// Install: npx shadcn@latest add command
import {
Command,
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from "@/components/ui/command"
export function CommandMenu() {
return (
<Command>
<CommandInput placeholder="Type a command..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>Calendar</CommandItem>
<CommandItem>Search</CommandItem>
<CommandItem>Calculator</CommandItem>
</CommandGroup>
<CommandSeparator />
<CommandGroup heading="Settings">
<CommandItem>Profile</CommandItem>
<CommandItem>Billing</CommandItem>
<CommandItem>Settings</CommandItem>
</CommandGroup>
</CommandList>
</Command>
)
}Table 表格组件
Table 组件提供简洁的样式表格,支持表头、主体行、页脚和标题。
// Install: npx shadcn@latest add table
import {
Table, TableBody, TableCaption,
TableCell, TableHead, TableHeader, TableRow,
} from "@/components/ui/table"
export function TableExample() {
return (
<Table>
<TableCaption>A list of recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead>Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV001</TableCell>
<TableCell>Paid</TableCell>
<TableCell>Credit Card</TableCell>
<TableCell>\$250.00</TableCell>
</TableRow>
</TableBody>
</Table>
)
}主题和暗黑模式
shadcn/ui 使用 CSS 变量进行主题设置,轻松创建自定义配色方案并支持暗黑模式。所有颜色使用 HSL 值定义。
/* globals.css - CSS Variables for theming */
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}暗黑模式通过在 .dark 类中定义第二组 CSS 变量实现。切换 dark 类时,所有组件自动切换到暗黑模式颜色。
设置主题提供器
使用 next-themes(Next.js)或自定义提供器在明暗模式间切换。
// npm install next-themes
// components/theme-provider.tsx
"use client"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes"
export function ThemeProvider({
children, ...props
}: ThemeProviderProps) {
return (
<NextThemesProvider {...props}>
{children}
</NextThemesProvider>
)
}
// app/layout.tsx
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}CSS 变量自定义
因为你拥有组件代码,自定义很简单。修改 CSS 变量进行全局更改,或编辑单个组件文件进行特定更改。
CSS 变量方法意味着只需更改几个 HSL 值就能创建全新主题。命名约定使用语义名称如 background、foreground、primary 等。
/* Example: Creating a custom blue theme */
@layer base {
:root {
--primary: 221.2 83.2% 53.3%; /* Blue */
--primary-foreground: 210 40% 98%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.75rem; /* Rounder corners */
}
}
/* Example: Creating a brand-colored theme */
@layer base {
:root {
/* Convert your brand color to HSL */
/* Brand color: #6366f1 = 239 84% 67% */
--primary: 239 84% 67%;
--primary-foreground: 0 0% 100%;
--ring: 239 84% 67%;
}
}React Hook Form + Zod 表单
shadcn/ui 提供强大的表单系统,集成 React Hook Form 进行状态管理和 Zod 进行基于模式的验证,实现类型安全的表单和自动错误处理。
构建表单的步骤:1) 定义 Zod 模式,2) 使用 useForm 和 zodResolver 创建表单,3) 使用 FormField 连接字段,4) 处理提交。
// Install dependencies:
// npx shadcn@latest add form input button
// npm install zod @hookform/resolvers
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { z } from "zod"
import { Button } from "@/components/ui/button"
import {
Form, FormControl, FormDescription,
FormField, FormItem, FormLabel, FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
// Step 1: Define Zod schema
const formSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
email: z.string().email({
message: "Please enter a valid email address.",
}),
})
// Step 2: Create the form component
export function ProfileForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
// Step 3: Render form fields
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
Your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}TanStack Table 数据表格
DataTable 组件将 shadcn/ui Table 与 TanStack Table 结合,提供排序、筛选、分页和行选择功能。
构建数据表格:1) 定义列,2) 创建使用 useReactTable 的 DataTable 组件,3) 使用 flexRender 渲染。
// Install: npx shadcn@latest add table
// npm install @tanstack/react-table
import {
ColumnDef,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table"
import {
Table, TableBody, TableCell,
TableHead, TableHeader, TableRow,
} from "@/components/ui/table"
// Step 1: Define columns
type Payment = {
id: string
amount: number
status: "pending" | "processing" | "success" | "failed"
email: string
}
const columns: ColumnDef<Payment>[] = [
{
accessorKey: "status",
header: "Status",
},
{
accessorKey: "email",
header: "Email",
},
{
accessorKey: "amount",
header: () => <div>Amount</div>,
cell: ({ row }) => {
const amount = parseFloat(row.getValue("amount"))
const formatted = new Intl.NumberFormat("en-US", {
style: "currency",
currency: "USD",
}).format(amount)
return <div>{formatted}</div>
},
},
]
// Step 2: Create DataTable component
function DataTable<TData, TValue>({
columns, data,
}: {
columns: ColumnDef<TData, TValue>[]
data: TData[]
}) {
const [sorting, setSorting] = React.useState<SortingState>([])
const table = useReactTable({
data,
columns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
state: { sorting },
})
return (
<Table>
<TableHeader>
{table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id}>
{hg.headers.map((header) => (
<TableHead key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
)
}无障碍特性
基于 Radix UI 原语构建,每个组件都遵循 WAI-ARIA 指南,包括正确的角色、键盘导航、焦点管理和屏幕阅读器支持。
- 所有交互组件支持完整键盘导航(Tab、Enter、Space、方向键、Escape)
- 焦点在模态对话框内被捕获,关闭时返回触发元素
- ARIA 属性自动管理
- 屏幕阅读器播报组件状态变化
- 默认主题的颜色对比度符合 WCAG 2.1 AA 标准
- 组件处理触摸目标和移动设备的响应行为
// Accessibility is built-in with Radix UI
// Example: Dialog with proper focus management
<Dialog>
<DialogTrigger asChild>
{/* Trigger receives focus on close */}
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
{/* Focus is trapped inside */}
{/* Escape key closes the dialog */}
{/* aria-describedby auto-set */}
<DialogTitle>Accessible Dialog</DialogTitle>
<DialogDescription>
Screen readers announce this description.
</DialogDescription>
<Button>First focusable element</Button>
</DialogContent>
</Dialog>创建自定义组件
你可以按照 shadcn/ui 模式创建自己的组件:使用 Radix UI 处理行为,Tailwind CSS 处理样式,cn() 工具处理条件类名。
// Example: Custom StatusBadge component
// following the shadcn/ui pattern
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const statusBadgeVariants = cva(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring",
{
variants: {
status: {
online: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300",
offline: "bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-300",
busy: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300",
away: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300",
},
},
defaultVariants: {
status: "offline",
},
}
)
interface StatusBadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof statusBadgeVariants> {}
function StatusBadge({ status, ...props }: StatusBadgeProps) {
return (
<div
{...props}
// cn() merges classes without conflicts
/>
)
}
export { StatusBadge, statusBadgeVariants }Tailwind CSS 集成
shadcn/ui 与 Tailwind CSS 无缝配合。cn() 工具函数(基于 clsx 和 tailwind-merge)是组合类名的关键。
tailwind-merge 智能解决类名冲突,确保自定义类正确覆盖组件默认类。
// src/lib/utils.ts - The cn() utility
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// Usage: combining component + custom classes
import { cn } from "@/lib/utils"
// Without cn(): "bg-primary bg-red-500"
// Both classes applied = unpredictable result
// With cn(): "bg-red-500"
// tailwind-merge resolves the conflict
function MyComponent({ isActive }: { isActive: boolean }) {
return (
<div
// cn() handles conditional classes cleanly
>
Content
</div>
)
}从其他 UI 库迁移
从 Material UI、Chakra UI 或 Ant Design 迁移时,shadcn/ui 支持渐进式迁移,可以逐个组件替换。
迁移策略:1) 并行安装,2) 从简单组件开始替换,3) 迁移表单组件,4) 最后迁移复杂组件,5) 移除旧库。
| Feature | shadcn/ui | Material UI | Chakra UI |
|---|---|---|---|
| Installation | Copy to project | npm dependency | npm dependency |
| Styling | Tailwind CSS | Emotion / styled | Emotion / styled |
| Code ownership | Full ownership | Library-owned | Library-owned |
| Bundle size | Only what you use | Tree-shakeable | Tree-shakeable |
| Customization | Edit source directly | Theme API + sx prop | Theme API + style props |
| Accessibility | Radix UI (WAI-ARIA) | Built-in (WAI-ARIA) | Built-in (WAI-ARIA) |
| Update strategy | Manual (re-run CLI) | npm update | npm update |
| Dark mode | CSS variables | ThemeProvider | ColorModeProvider |
最佳实践
- 使用 CLI 添加组件 — 不要从 GitHub 手动复制
- 将 components.json 纳入版本控制
- 使用 cn() 工具处理所有条件类名逻辑
- 在 globals.css 中自定义 CSS 变量以保持主题一致
- 除非理解无障碍影响,否则不要修改 Radix UI 原语行为
- 所有表单使用 React Hook Form + Zod
- 用自己的组件包装 shadcn/ui 组件以设置项目默认值
- 保持组件目录整洁 — 将相关组件分组到子目录
- 定期重新运行 CLI add 命令更新组件
- 使用 new-york 样式变体获得更精致紧凑的设计
常见问题
shadcn/ui 是组件库吗?
不是传统意义上的组件库。它是你复制粘贴到项目中的可复用组件集合,不作为 npm 依赖安装。你拥有代码,可以自定义一切。
如何更新 shadcn/ui 组件?
由于组件被复制到项目中,更新是手动的。重新运行 CLI add 命令,然后审查差异以合并更改。
可以不用 Tailwind CSS 使用 shadcn/ui 吗?
shadcn/ui 专为 Tailwind CSS 设计。如果不用 Tailwind,建议直接使用 Radix UI 配合你自己的样式方案。
shadcn/ui 可以用于生产环境吗?
可以,shadcn/ui 广泛用于生产环境。组件基于经过实战检验的 Radix UI 原语构建。
shadcn/ui 与 Material UI 或 Chakra UI 有何不同?
MUI 和 Chakra 作为依赖安装并提供自己的样式系统。shadcn/ui 使用 Tailwind CSS 并给你实际的源代码,提供更多控制和灵活性。
shadcn/ui 支持 React Server Components 吗?
静态组件如 Table、Badge 和 Card 可以作为服务器组件。交互组件需要 use client 指令。
可以在 Vue 或 Svelte 中使用 shadcn/ui 吗?
shadcn/ui 专为 React 构建,但社区已移植到 Vue(shadcn-vue)和 Svelte(shadcn-svelte)。
如何处理 shadcn/ui 中的组件依赖?
添加依赖其他组件的组件时,CLI 会自动安装依赖。例如添加 Combobox 会自动安装 Popover、Command 和 Button。