shadcn-ui
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chineseshadcn/ui - Component Library
shadcn/ui - 组件库
progressive_disclosure: entry_point: summary, when_to_use, quick_start estimated_tokens: entry: 85 full: 4800
progressive_disclosure: entry_point: summary, when_to_use, quick_start estimated_tokens: entry: 85 full: 4800
Summary
概述
shadcn/ui is a collection of re-usable React components built with Radix UI primitives and styled with Tailwind CSS. Unlike traditional component libraries, shadcn/ui components are copied directly into your project, giving you full ownership and customization control. Components are accessible, customizable, and open source.
Core Philosophy: Copy-paste components, not npm packages. You own the code.
shadcn/ui是一组基于Radix UI原语构建、用Tailwind CSS样式化的可复用React组件。与传统组件库不同,shadcn/ui组件可直接复制到你的项目中,让你完全拥有代码所有权和自定义控制权。组件具备无障碍访问能力、可自定义且开源。
核心理念:复制粘贴组件,而非安装npm包。你拥有代码所有权。
When to Use
适用场景
Use shadcn/ui when:
- Building React applications with Tailwind CSS
- Need accessible, production-ready UI components
- Want full control over component code and styling
- Prefer composition over configuration
- Building with Next.js, Vite, Remix, or Astro
- Need dark mode support out of the box
- Want TypeScript-first components
Don't use when:
- Not using Tailwind CSS (core styling dependency)
- Need legacy browser support (uses modern CSS features)
- Prefer packaged npm libraries over code ownership
- Building non-React frameworks (Vue, Svelte, Angular)
适合使用shadcn/ui的场景:
- 构建基于Tailwind CSS的React应用
- 需要具备无障碍访问能力、可用于生产环境的UI组件
- 希望完全控制组件代码和样式
- 偏好组合而非配置的开发方式
- 使用Next.js、Vite、Remix或Astro框架
- 需要开箱即用的深色模式支持
- 优先选择TypeScript组件
不适合使用的场景:
- 未使用Tailwind CSS(核心样式依赖)
- 需要兼容旧版浏览器(使用了现代CSS特性)
- 偏好封装好的npm库而非代码所有权
- 构建非React框架的应用(Vue、Svelte、Angular)
Quick Start
快速开始
Installation
安装
bash
undefinedbash
undefinedInitialize shadcn/ui in your project
在你的项目中初始化shadcn/ui
npx shadcn-ui@latest init
npx shadcn-ui@latest init
Follow interactive prompts:
跟随交互式提示操作:
- TypeScript? (yes/no)
- 是否使用TypeScript?(是/否)
- Style: Default/New York
- 样式风格:默认/纽约风
- Base color: Slate/Gray/Zinc/Neutral/Stone
- 基础颜色:Slate/Gray/Zinc/Neutral/Stone
- CSS variables: (yes/no)
- 是否使用CSS变量?(是/否)
- React Server Components: (yes/no)
- 是否支持React Server Components?(是/否)
- components.json location
- components.json文件位置
- Tailwind config location
- Tailwind配置文件位置
- CSS file location
- CSS文件位置
- Import alias (@/components)
- 导入别名(@/components)
undefinedundefinedAdd Your First Component
添加第一个组件
bash
undefinedbash
undefinedAdd individual components
添加单个组件
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add dialog
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add dialog
Add multiple components at once
同时添加多个组件
npx shadcn-ui@latest add button card dialog form input
undefinednpx shadcn-ui@latest add button card dialog form input
undefinedBasic Usage
基础用法
tsx
import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
export default function Example() {
return (
<Card>
<CardHeader>
<CardTitle>Welcome</CardTitle>
</CardHeader>
<CardContent>
<Button>Click me</Button>
</CardContent>
</Card>
)
}tsx
import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
export default function Example() {
return (
<Card>
<CardHeader>
<CardTitle>Welcome</CardTitle>
</CardHeader>
<CardContent>
<Button>Click me</Button>
</CardContent>
</Card>
)
}Architecture
架构
Copy-Paste Philosophy
复制粘贴理念
Key Difference from Traditional Libraries:
- Traditional: → locked to package versions
npm install component-library - shadcn/ui: Components copied to → you own the code
components/ui/
Benefits:
- Full customization control
- No breaking changes from package updates
- Easy to modify for specific needs
- Transparent implementation
- Tree-shakeable by default
与传统组件库的核心区别:
- 传统方式:→ 受限于包版本
npm install component-library - shadcn/ui:组件复制到目录 → 你拥有代码所有权
components/ui/
优势:
- 完全的自定义控制权
- 不会因包更新导致破坏性变更
- 可轻松修改以满足特定需求
- 实现透明化
- 默认支持摇树优化
Component Structure
组件结构
src/
├── components/
│ └── ui/
│ ├── button.tsx # Component implementation
│ ├── card.tsx # Owns its code
│ ├── dialog.tsx # Modifiable
│ └── ...
├── lib/
│ └── utils.ts # cn() helper for class merging
└── app/
└── globals.css # Tailwind directives + CSS variablessrc/
├── components/
│ └── ui/
│ ├── button.tsx # 组件实现代码
│ ├── card.tsx # 你拥有代码所有权
│ ├── dialog.tsx # 可修改
│ └── ...
├── lib/
│ └── utils.ts # 用于类名合并的cn()工具函数
└── app/
└── globals.css # Tailwind指令 + CSS变量Technology Stack
技术栈
Core Dependencies:
- Radix UI: Accessible component primitives (headless UI)
- Tailwind CSS: Utility-first styling
- TypeScript: Type safety
- class-variance-authority (CVA): Variant management
- clsx: Class name concatenation
- tailwind-merge: Conflict-free class merging
Radix UI Integration:
tsx
// shadcn/ui components wrap Radix primitives
import * as DialogPrimitive from "@radix-ui/react-dialog"
// Add styling and variants
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogContent = React.forwardRef<...>(
({ className, children, ...props }, ref) => (
<DialogPrimitive.Content
ref={ref}
className={cn("fixed ...", className)}
{...props}
/>
)
)核心依赖:
- Radix UI:具备无障碍访问能力的组件原语(无头UI)
- Tailwind CSS:实用优先的样式框架
- TypeScript:类型安全
- class-variance-authority (CVA):变体管理
- clsx:类名拼接
- tailwind-merge:无冲突的类名合并
Radix UI集成示例:
tsx
// shadcn/ui组件封装了Radix原语
import * as DialogPrimitive from "@radix-ui/react-dialog"
// 添加样式和变体
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogContent = React.forwardRef<...>(
({ className, children, ...props }, ref) => (
<DialogPrimitive.Content
ref={ref}
className={cn("fixed ...", className)}
{...props}
/>
)
)Configuration
配置
components.json
components.json
json
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}Key Options:
- : "default" or "new-york" (design variants)
style - : React Server Components support
rsc - : Use CSS variables for theming
cssVariables - : Tailwind class prefix (optional)
prefix
json
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}关键配置项:
- :"default"或"new-york"(设计变体)
style - :是否支持React Server Components
rsc - :是否使用CSS变量进行主题配置
cssVariables - :Tailwind类名前缀(可选)
prefix
Tailwind Configuration
Tailwind配置
ts
// tailwind.config.ts
import type { Config } from "tailwindcss"
const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config
export default configts
// tailwind.config.ts
import type { Config } from "tailwindcss"
const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config
export default configCSS Variables (globals.css)
CSS变量(globals.css)
css
@tailwind base;
@tailwind components;
@tailwind utilities;
@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%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-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%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}css
@tailwind base;
@tailwind components;
@tailwind utilities;
@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%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-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%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}Component Catalog
组件目录
Button
按钮(Button)
tsx
import { Button } from "@/components/ui/button"
// Variants
<Button variant="default">Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
// Sizes
<Button size="default">Default</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Icon /></Button>
// States
<Button disabled>Disabled</Button>
<Button asChild>
<Link href="/about">As Link</Link>
</Button>Implementation Pattern (CVA):
tsx
import { cva, type VariantProps } from "class-variance-authority"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
{
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 hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
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",
},
}
)tsx
import { Button } from "@/components/ui/button"
// 变体
<Button variant="default">默认按钮</Button>
<Button variant="destructive">危险按钮</Button>
<Button variant="outline">轮廓按钮</Button>
<Button variant="secondary">次要按钮</Button>
<Button variant="ghost">幽灵按钮</Button>
<Button variant="link">链接按钮</Button>
// 尺寸
<Button size="default">默认尺寸</Button>
<Button size="sm">小尺寸</Button>
<Button size="lg">大尺寸</Button>
<Button size="icon"><Icon /></Button>
// 状态
<Button disabled>禁用状态</Button>
<Button asChild>
<Link href="/about">作为链接</Link>
</Button>实现模式(CVA):
tsx
import { cva, type VariantProps } from "class-variance-authority"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
{
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 hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
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",
},
}
)Card
卡片(Card)
tsx
import {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card"
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description goes here</CardDescription>
</CardHeader>
<CardContent>
<p>Card content</p>
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>tsx
import {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card"
<Card>
<CardHeader>
<CardTitle>卡片标题</CardTitle>
<CardDescription>卡片描述信息</CardDescription>
</CardHeader>
<CardContent>
<p>卡片内容</p>
</CardContent>
<CardFooter>
<Button>操作按钮</Button>
</CardFooter>
</Card>Dialog (Modal)
对话框(Dialog/Modal)
tsx
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "@/components/ui/dialog"
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>tsx
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "@/components/ui/dialog"
<Dialog>
<DialogTrigger asChild>
<Button>打开对话框</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>你确定吗?</DialogTitle>
<DialogDescription>
此操作无法撤销。
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">取消</Button>
<Button>确认</Button>
</DialogFooter>
</DialogContent>
</Dialog>Form (with react-hook-form + zod)
表单(Form,基于react-hook-form + zod)
tsx
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
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.",
}),
})
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)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="user@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}tsx
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
const formSchema = z.object({
username: z.string().min(2, {
message: "用户名至少需要2个字符。",
}),
email: z.string().email({
message: "请输入有效的邮箱地址。",
}),
})
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)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>用户名</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
这是你的公开显示名称。
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>邮箱</FormLabel>
<FormControl>
<Input type="email" placeholder="user@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">提交</Button>
</form>
</Form>
)
}Table
表格(Table)
tsx
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV001</TableCell>
<TableCell>Paid</TableCell>
<TableCell>Credit Card</TableCell>
<TableCell className="text-right">$250.00</TableCell>
</TableRow>
</TableBody>
</Table>tsx
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
<Table>
<TableCaption>你最近的发票列表。</TableCaption>
<TableHeader>
<TableRow>
<TableHead>发票编号</TableHead>
<TableHead>状态</TableHead>
<TableHead>支付方式</TableHead>
<TableHead className="text-right">金额</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV001</TableCell>
<TableCell>已支付</TableCell>
<TableCell>信用卡</TableCell>
<TableCell className="text-right">$250.00</TableCell>
</TableRow>
</TableBody>
</Table>Additional Components
更多组件
Available via CLI:
- - Collapsible content sections
accordion - - Contextual feedback messages
alert - - Interrupting modal dialogs
alert-dialog - - User profile images
avatar - - Status indicators
badge - - Date picker
calendar - - Binary input
checkbox - - Command palette (⌘K menu)
command - - Right-click menus
context-menu - - Dropdown menus
dropdown-menu - - Hover tooltips
hover-card - - Text input
input - - Form labels
label - - Application menu bar
menubar - - Site navigation
navigation-menu - - Floating panels
popover - - Progress indicators
progress - - Radio button groups
radio-group - - Custom scrollbars
scroll-area - - Dropdown selects
select - - Visual dividers
separator - - Side panels
sheet - - Loading placeholders
skeleton - - Range input
slider - - Toggle switch
switch - - Tab navigation
tabs - - Multi-line input
textarea - - Notification toasts
toast - - Toggle button
toggle - - Hover tooltips
tooltip
可通过CLI添加的组件:
- - 可折叠内容区块
accordion - - 上下文反馈消息
alert - - 中断式模态对话框
alert-dialog - - 用户头像
avatar - - 状态指示器
badge - - 日期选择器
calendar - - 复选框
checkbox - - 命令面板(⌘K菜单)
command - - 右键菜单
context-menu - - 下拉菜单
dropdown-menu - - 悬停提示卡片
hover-card - - 文本输入框
input - - 表单标签
label - - 应用菜单栏
menubar - - 站点导航菜单
navigation-menu - - 浮动面板
popover - - 进度指示器
progress - - 单选按钮组
radio-group - - 自定义滚动区域
scroll-area - - 下拉选择器
select - - 视觉分隔线
separator - - 侧边面板
sheet - - 加载占位符
skeleton - - 范围滑块
slider - - 开关按钮
switch - - 标签页导航
tabs - - 多行文本输入框
textarea - - 通知提示框
toast - - 切换按钮
toggle - - 悬停提示
tooltip
Theming
主题定制
Color Customization
颜色自定义
Change base color scheme:
bash
undefined更改基础配色方案:
bash
undefinedRegenerate components with new base color
使用新的基础颜色重新生成组件
npx shadcn-ui@latest init
npx shadcn-ui@latest init
Choose new base: Slate, Gray, Zinc, Neutral, Stone
选择新的基础颜色:Slate、Gray、Zinc、Neutral、Stone
**Manual color override** (globals.css):
```css
:root {
--primary: 210 100% 50%; /* HSL: Blue */
--primary-foreground: 0 0% 100%;
}
.dark {
--primary: 210 100% 60%; /* Lighter blue for dark mode */
}
**手动覆盖颜色(globals.css)**:
```css
:root {
--primary: 210 100% 50%; /* HSL:蓝色 */
--primary-foreground: 0 0% 100%;
}
.dark {
--primary: 210 100% 60%; /* 深色模式下的浅蓝 */
}Custom Variants
自定义变体
tsx
// Extend button variants
const buttonVariants = cva(
"...",
{
variants: {
variant: {
// ...existing variants
gradient: "bg-gradient-to-r from-purple-500 to-pink-500 text-white",
},
},
}
)
// Usage
<Button variant="gradient">Gradient Button</Button>tsx
// 扩展按钮变体
const buttonVariants = cva(
"...",
{
variants: {
variant: {
// ...现有变体
gradient: "bg-gradient-to-r from-purple-500 to-pink-500 text-white",
},
},
}
)
// 使用方式
<Button variant="gradient">渐变按钮</Button>Theme Switching
主题切换
tsx
// Using next-themes
import { ThemeProvider } from "next-themes"
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}
// Theme toggle component
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ThemeToggle() {
const { setTheme, theme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}tsx
// 使用next-themes库
import { ThemeProvider } from "next-themes"
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}
// 主题切换组件
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ThemeToggle() {
const { setTheme, theme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">切换主题</span>
</Button>
)
}Dark Mode
深色模式
Setup with Next.js
Next.js配置
bash
npm install next-themestsx
// app/providers.tsx
"use client"
import { ThemeProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
)
}
// app/layout.tsx
import { Providers } from "./providers"
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}bash
npm install next-themestsx
// app/providers.tsx
"use client"
import { ThemeProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
)
}
// app/layout.tsx
import { Providers } from "./providers"
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}Dark Mode Utilities
深色模式工具
tsx
// Force dark mode for specific section
<div className="dark">
<Card>Always dark, regardless of theme</Card>
</div>
// Conditional styling
<div className="bg-white dark:bg-slate-950">
<p className="text-slate-900 dark:text-slate-50">
Adapts to theme
</p>
</div>tsx
// 强制特定区域使用深色模式
<div className="dark">
<Card>始终为深色,不受主题影响</Card>
</div>
// 条件样式
<div className="bg-white dark:bg-slate-950">
<p className="text-slate-900 dark:text-slate-50">
适配主题的文本
</p>
</div>Next.js Integration
Next.js集成
App Router Setup
App Router配置
bash
undefinedbash
undefinedCreate Next.js app with TypeScript and Tailwind
创建带有TypeScript和Tailwind的Next.js应用
npx create-next-app@latest my-app --typescript --tailwind --app
npx create-next-app@latest my-app --typescript --tailwind --app
Initialize shadcn/ui
初始化shadcn/ui
cd my-app
npx shadcn-ui@latest init
cd my-app
npx shadcn-ui@latest init
Add components
添加组件
npx shadcn-ui@latest add button card form
undefinednpx shadcn-ui@latest add button card form
undefinedServer Components
服务端组件
tsx
// app/page.tsx (Server Component by default)
import { Button } from "@/components/ui/button"
export default function HomePage() {
return (
<main>
<h1>Welcome</h1>
{/* Static components work in Server Components */}
<Button asChild>
<a href="/about">Learn More</a>
</Button>
</main>
)
}tsx
// app/page.tsx(默认是服务端组件)
import { Button } from "@/components/ui/button"
export default function HomePage() {
return (
<main>
<h1>欢迎</h1>
{/* 静态组件可在服务端组件中使用 */}
<Button asChild>
<a href="/about">了解更多</a>
</Button>
</main>
)
}Client Components
客户端组件
tsx
// app/interactive.tsx
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"
export function InteractiveSection() {
const [open, setOpen] = useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<p>Client-side interactivity</p>
</DialogContent>
</Dialog>
)
}tsx
// app/interactive.tsx
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"
export function InteractiveSection() {
const [open, setOpen] = useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>打开对话框</Button>
</DialogTrigger>
<DialogContent>
<p>客户端交互内容</p>
</DialogContent>
</Dialog>
)
}Route Handlers
路由处理器
tsx
// app/api/submit/route.ts
import { NextResponse } from "next/server"
import { z } from "zod"
const formSchema = z.object({
email: z.string().email(),
message: z.string().min(10),
})
export async function POST(request: Request) {
try {
const body = await request.json()
const validatedData = formSchema.parse(body)
// Process form data
return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ errors: error.errors }, { status: 400 })
}
return NextResponse.json({ error: "Internal error" }, { status: 500 })
}
}tsx
// app/api/submit/route.ts
import { NextResponse } from "next/server"
import { z } from "zod"
const formSchema = z.object({
email: z.string().email(),
message: z.string().min(10),
})
export async function POST(request: Request) {
try {
const body = await request.json()
const validatedData = formSchema.parse(body)
// 处理表单数据
return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ errors: error.errors }, { status: 400 })
}
return NextResponse.json({ error: "内部错误" }, { status: 500 })
}
}Accessibility
无障碍访问
ARIA Support
ARIA支持
All shadcn/ui components include proper ARIA attributes via Radix UI:
tsx
// Dialog automatically includes:
// - role="dialog"
// - aria-describedby
// - aria-labelledby
// - Focus trap
// - Escape key handler
<Dialog>
<DialogContent>
{/* Automatically accessible */}
</DialogContent>
</Dialog>
// Button includes:
// - role="button"
// - tabindex="0"
// - Keyboard activation (Space/Enter)
<Button>Accessible by default</Button>所有shadcn/ui组件通过Radix UI内置了完善的ARIA属性:
tsx
// 对话框自动包含:
// - role="dialog"
// - aria-describedby
// - aria-labelledby
// - 焦点陷阱
// - ESC键关闭处理
<Dialog>
<DialogContent>
{/* 自动具备无障碍访问能力 */}
</DialogContent>
</Dialog>
// 按钮包含:
// - role="button"
// - tabindex="0"
// - 键盘激活支持(空格/回车)
<Button>默认具备无障碍访问能力</Button>Keyboard Navigation
键盘导航
Built-in keyboard support:
- /
Tab- Navigate between interactive elementsShift+Tab - /
Enter- Activate buttonsSpace - - Close dialogs, dropdowns, popovers
Escape - - Navigate menus, select options, radio groups
Arrow keys - /
Home- Jump to first/last in listsEnd
Example: Command Palette:
tsx
import {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
} from "@/components/ui/command"
// ⌘K to open
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>Calendar</CommandItem>
<CommandItem>Search Emoji</CommandItem>
<CommandItem>Calculator</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>内置键盘支持:
- /
Tab- 在交互式元素间导航Shift+Tab - /
Enter- 激活按钮Space - - 关闭对话框、下拉菜单、浮动面板
Escape - 方向键 - 导航菜单、选择选项、单选按钮组
- /
Home- 跳转到列表的第一个/最后一个元素End
示例:命令面板:
tsx
import {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
} from "@/components/ui/command"
// 按⌘K打开
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="输入命令..." />
<CommandList>
<CommandEmpty>未找到结果。</CommandEmpty>
<CommandGroup heading="推荐">
<CommandItem>日历</CommandItem>
<CommandItem>搜索表情</CommandItem>
<CommandItem>计算器</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>Screen Reader Support
屏幕阅读器支持
tsx
// Visually hidden but accessible to screen readers
<span className="sr-only">Close dialog</span>
// Skip navigation links
<a href="#main-content" className="sr-only focus:not-sr-only">
Skip to main content
</a>
// Descriptive labels
<FormLabel htmlFor="email">Email address</FormLabel>
<Input
id="email"
type="email"
aria-describedby="email-description"
aria-invalid={!!errors.email}
/>
<FormDescription id="email-description">
We'll never share your email.
</FormDescription>tsx
// 视觉隐藏但屏幕阅读器可访问
<span className="sr-only">关闭对话框</span>
// 跳过导航链接
<a href="#main-content" className="sr-only focus:not-sr-only">
跳转到主要内容
</a>
// 描述性标签
<FormLabel htmlFor="email">邮箱地址</FormLabel>
<Input
id="email"
type="email"
aria-describedby="email-description"
aria-invalid={!!errors.email}
/>
<FormDescription id="email-description">
我们绝不会分享你的邮箱。
</FormDescription>Focus Management
焦点管理
tsx
// Focus trap in Dialog (automatic)
<Dialog>
<DialogContent>
{/* Focus stays within dialog until closed */}
</DialogContent>
</Dialog>
// Custom focus management
import { useRef, useEffect } from "react"
function CustomComponent() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return <Input ref={inputRef} />
}tsx
// 对话框自动实现焦点陷阱
<Dialog>
<DialogContent>
{/* 焦点会一直停留在对话框内直到关闭 */}
</DialogContent>
</Dialog>
// 自定义焦点管理
import { useRef, useEffect } from "react"
function CustomComponent() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return <Input ref={inputRef} />
}Composition Patterns
组合模式
Compound Components
复合组件
tsx
// Card composition
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardContent>Content</CardContent>
<CardFooter>Footer</CardFooter>
</Card>
// Form composition
<Form {...form}>
<FormField
control={form.control}
name="field"
render={({ field }) => (
<FormItem>
<FormLabel>Label</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>Help text</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</Form>tsx
// 卡片组合
<Card>
<CardHeader>
<CardTitle>标题</CardTitle>
<CardDescription>描述</CardDescription>
</CardHeader>
<CardContent>内容</CardContent>
<CardFooter>页脚</CardFooter>
</Card>
// 表单组合
<Form {...form}>
<FormField
control={form.control}
name="field"
render={({ field }) => (
<FormItem>
<FormLabel>标签</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>帮助文本</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</Form>Polymorphic Components (asChild)
多态组件(asChild)
tsx
// Render Button as Link
import { Button } from "@/components/ui/button"
import Link from "next/link"
<Button asChild>
<Link href="/dashboard">Go to Dashboard</Link>
</Button>
// Render as custom component
<Button asChild>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Animated Button
</motion.button>
</Button>How it works (Radix Slot):
tsx
import { Slot } from "@radix-ui/react-slot"
interface ButtonProps {
asChild?: boolean
}
const Button = ({ asChild, ...props }: ButtonProps) => {
const Comp = asChild ? Slot : "button"
return <Comp {...props} />
}tsx
// 将按钮渲染为链接
import { Button } from "@/components/ui/button"
import Link from "next/link"
<Button asChild>
<Link href="/dashboard">前往控制台</Link>
</Button>
// 渲染为自定义组件
<Button asChild>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
带动画的按钮
</motion.button>
</Button>实现原理(Radix Slot):
tsx
import { Slot } from "@radix-ui/react-slot"
interface ButtonProps {
asChild?: boolean
}
const Button = ({ asChild, ...props }: ButtonProps) => {
const Comp = asChild ? Slot : "button"
return <Comp {...props} />
}Custom Compositions
自定义组合
tsx
// Create custom card variant
export function PricingCard({
title,
price,
features,
highlighted
}: PricingCardProps) {
return (
<Card className={cn(highlighted && "border-primary")}>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription className="text-3xl font-bold">
${price}/mo
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{features.map((feature) => (
<li key={feature} className="flex items-center">
<Check className="mr-2 h-4 w-4 text-primary" />
{feature}
</li>
))}
</ul>
</CardContent>
<CardFooter>
<Button className="w-full" variant={highlighted ? "default" : "outline"}>
Get Started
</Button>
</CardFooter>
</Card>
)
}tsx
// 创建自定义卡片变体
export function PricingCard({
title,
price,
features,
highlighted
}: PricingCardProps) {
return (
<Card className={cn(highlighted && "border-primary")}>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription className="text-3xl font-bold">
${price}/月
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{features.map((feature) => (
<li key={feature} className="flex items-center">
<Check className="mr-2 h-4 w-4 text-primary" />
{feature}
</li>
))}
</ul>
</CardContent>
<CardFooter>
<Button className="w-full" variant={highlighted ? "default" : "outline"}>
开始使用
</Button>
</CardFooter>
</Card>
)
}CLI Commands
CLI命令
Initialize
初始化
bash
undefinedbash
undefinedInteractive init
交互式初始化
npx shadcn-ui@latest init
npx shadcn-ui@latest init
Non-interactive with defaults
使用默认值非交互式初始化
npx shadcn-ui@latest init -y
npx shadcn-ui@latest init -y
Specify options
指定配置选项
npx shadcn-ui@latest init --typescript --tailwind
undefinednpx shadcn-ui@latest init --typescript --tailwind
undefinedAdd Components
添加组件
bash
undefinedbash
undefinedSingle component
添加单个组件
npx shadcn-ui@latest add button
npx shadcn-ui@latest add button
Multiple components
添加多个组件
npx shadcn-ui@latest add button card dialog form
npx shadcn-ui@latest add button card dialog form
All components (not recommended - adds everything)
添加所有组件(不推荐,会添加全部内容)
npx shadcn-ui@latest add --all
npx shadcn-ui@latest add --all
Specific version
添加指定版本的组件
npx shadcn-ui@latest add button@1.0.0
npx shadcn-ui@latest add button@1.0.0
Overwrite existing
覆盖现有组件
npx shadcn-ui@latest add button --overwrite
npx shadcn-ui@latest add button --overwrite
Different path
指定路径添加
npx shadcn-ui@latest add button --path src/components/ui
undefinednpx shadcn-ui@latest add button --path src/components/ui
undefinedDiff Components
对比组件差异
bash
undefinedbash
undefinedCheck for component updates
检查组件更新
npx shadcn-ui@latest diff
npx shadcn-ui@latest diff
Diff specific component
对比指定组件的差异
npx shadcn-ui@latest diff button
npx shadcn-ui@latest diff button
Show what would change
仅显示即将变更的内容
npx shadcn-ui@latest diff --check
undefinednpx shadcn-ui@latest diff --check
undefinedUpdate Components
更新组件
bash
undefinedbash
undefinedUpdate all components
更新所有组件
npx shadcn-ui@latest update
npx shadcn-ui@latest update
Update specific components
更新指定组件
npx shadcn-ui@latest update button card
npx shadcn-ui@latest update button card
Preview changes before applying
预览变更内容而不实际应用
npx shadcn-ui@latest update --dry-run
undefinednpx shadcn-ui@latest update --dry-run
undefinedAdvanced Patterns
高级模式
Custom Hooks
自定义Hook
tsx
// useToast hook (built-in with toast component)
import { useToast } from "@/components/ui/use-toast"
function MyComponent() {
const { toast } = useToast()
return (
<Button
onClick={() => {
toast({
title: "Scheduled: Catch up",
description: "Friday, February 10, 2023 at 5:57 PM",
})
}}
>
Show Toast
</Button>
)
}
// Custom form hook
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
function useFormWithToast<T extends z.ZodType>(schema: T) {
const { toast } = useToast()
const form = useForm({
resolver: zodResolver(schema),
})
const handleSubmit = form.handleSubmit(async (data) => {
try {
// Submit logic
toast({ title: "Success!" })
} catch (error) {
toast({ title: "Error", variant: "destructive" })
}
})
return { form, handleSubmit }
}tsx
// useToast Hook(与toast组件内置)
import { useToast } from "@/components/ui/use-toast"
function MyComponent() {
const { toast } = useToast()
return (
<Button
onClick={() => {
toast({
title: "已安排:跟进事项",
description: "2023年2月10日周五下午5:57",
})
}}
>
显示提示
</Button>
)
}
// 自定义表单Hook
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
function useFormWithToast<T extends z.ZodType>(schema: T) {
const { toast } = useToast()
const form = useForm({
resolver: zodResolver(schema),
})
const handleSubmit = form.handleSubmit(async (data) => {
try {
// 提交逻辑
toast({ title: "成功!" })
} catch (error) {
toast({ title: "错误", variant: "destructive" })
}
})
return { form, handleSubmit }
}Responsive Design
响应式设计
tsx
// Mobile-first responsive components
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card>Mobile: 1 col, Tablet: 2 col, Desktop: 3 col</Card>
</div>
// Responsive dialog (sheet on mobile, dialog on desktop)
import { useMediaQuery } from "@/hooks/use-media-query"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { Sheet, SheetContent } from "@/components/ui/sheet"
function ResponsiveModal({ children, ...props }) {
const isDesktop = useMediaQuery("(min-width: 768px)")
if (isDesktop) {
return (
<Dialog {...props}>
<DialogContent>{children}</DialogContent>
</Dialog>
)
}
return (
<Sheet {...props}>
<SheetContent>{children}</SheetContent>
</Sheet>
)
}tsx
// 移动端优先的响应式组件
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card>移动端:1列,平板:2列,桌面端:3列</Card>
</div>
// 响应式对话框(移动端为侧边面板,桌面端为对话框)
import { useMediaQuery } from "@/hooks/use-media-query"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { Sheet, SheetContent } from "@/components/ui/sheet"
function ResponsiveModal({ children, ...props }) {
const isDesktop = useMediaQuery("(min-width: 768px)")
if (isDesktop) {
return (
<Dialog {...props}>
<DialogContent>{children}</DialogContent>
</Dialog>
)
}
return (
<Sheet {...props}>
<SheetContent>{children}</SheetContent>
</Sheet>
)
}Animation Variants
动画变体
tsx
// Using Framer Motion with shadcn/ui
import { motion } from "framer-motion"
import { Card } from "@/components/ui/card"
const MotionCard = motion(Card)
<MotionCard
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
Animated Card
</MotionCard>
// Staggered list animation
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
}
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
}
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((item) => (
<motion.li key={item.id} variants={item}>
<Card>{item.content}</Card>
</motion.li>
))}
</motion.ul>tsx
// 将shadcn/ui与Framer Motion结合使用
import { motion } from "framer-motion"
import { Card } from "@/components/ui/card"
const MotionCard = motion(Card)
<MotionCard
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
带动画的卡片
</MotionCard>
// 列表 stagger 动画
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
}
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
}
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((item) => (
<motion.li key={item.id} variants={item}>
<Card>{item.content}</Card>
</motion.li>
))}
</motion.ul>Best Practices
最佳实践
Code Organization:
- Keep shadcn/ui components in (don't mix with app components)
components/ui/ - Create custom compositions in (outside ui/)
components/ - Use for shared utilities
lib/utils.ts
Customization:
- Modify components directly in your project (you own the code)
- Use CSS variables for theme-wide changes
- Extend variants with CVA for new styles
- Don't edit manually (use CLI)
components.json
Performance:
- Tree-shaking automatic (only imports what you use)
- Use to avoid unnecessary wrapper elements
asChild - Lazy load heavy components (Calendar, Command)
- Prefer Server Components when possible (Next.js)
Accessibility:
- Don't remove ARIA attributes from components
- Test keyboard navigation for custom compositions
- Maintain focus management in dialogs/modals
- Use semantic HTML with when applicable
asChild
TypeScript:
- Leverage exported types (ButtonProps, CardProps, etc.)
- Use VariantProps for variant type safety
- Add strict null checks for form validation
代码组织:
- 将shadcn/ui组件放在目录下(不要与业务组件混合)
components/ui/ - 在目录下创建自定义组合组件(ui目录外)
components/ - 使用存放共享工具函数
lib/utils.ts
自定义:
- 直接在项目中修改组件代码(你拥有所有权)
- 使用CSS变量进行全局主题变更
- 通过CVA扩展变体来添加新样式
- 不要手动编辑(使用CLI操作)
components.json
性能:
- 自动支持摇树优化(仅导入你使用的内容)
- 使用避免不必要的包装元素
asChild - 懒加载重型组件(日历、命令面板)
- 尽可能使用服务端组件(Next.js)
无障碍访问:
- 不要移除组件的ARIA属性
- 测试自定义组合组件的键盘导航
- 保持对话框/模态框的焦点管理
- 使用时确保语义化HTML
asChild
TypeScript:
- 利用导出的类型(ButtonProps、CardProps等)
- 使用VariantProps确保变体类型安全
- 为表单验证添加严格的空值检查
Troubleshooting
故障排除
Import errors:
bash
undefined导入错误:
bash
undefinedCheck path aliases in tsconfig.json
检查tsconfig.json中的路径别名
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/": ["./src/"]
}
}
}
**Tailwind classes not applying**:
```ts
// Ensure content paths include your components
// tailwind.config.ts
content: [
'./src/components/**/*.{ts,tsx}', // Add this
'./src/app/**/*.{ts,tsx}',
]Dark mode not working:
tsx
// Add suppressHydrationWarning to <html>
<html lang="en" suppressHydrationWarning>Form validation not triggering:
tsx
// Ensure FormMessage is included in FormField
<FormField>
<FormItem>
<FormControl>...</FormControl>
<FormMessage /> {/* Required for errors */}
</FormItem>
</FormField>{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/": ["./src/"]
}
}
}
**Tailwind类不生效**:
```ts
#确保content路径包含你的组件Resources
tailwind.config.ts
- Official Docs: https://ui.shadcn.com
- Component Source: https://github.com/shadcn-ui/ui
- Radix UI Docs: https://www.radix-ui.com
- Tailwind CSS: https://tailwindcss.com
- CVA Docs: https://cva.style/docs
- Examples: https://ui.shadcn.com/examples
- Community: https://discord.gg/shadcn-ui
content: [
'./src/components//*.{ts,tsx}', // 添加此路径
'./src/app//*.{ts,tsx}',
]
**深色模式不工作**:
```tsx—
为<html>标签添加suppressHydrationWarning
—
<html lang="en" suppressHydrationWarning>
```
表单验证不触发:
tsx
undefined—
确保FormField中包含FormMessage
—
<FormField>
<FormItem>
<FormControl>...</FormControl>
<FormMessage /> {/* 错误提示需要此组件 */}
</FormItem>
</FormField>
```
—
资源
—
- 官方文档:https://ui.shadcn.com
- 组件源码:https://github.com/shadcn-ui/ui
- Radix UI文档:https://www.radix-ui.com
- Tailwind CSS:https://tailwindcss.com
- CVA文档:https://cva.style/docs
- 示例:https://ui.shadcn.com/examples
- 社区:https://discord.gg/shadcn-ui