shadcn
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chineseshadcn/ui Development Guidelines
shadcn/ui 开发指南
Best practices for using shadcn/ui components with Tailwind CSS and Radix UI primitives.
结合Tailwind CSS和Radix UI基础组件使用shadcn/ui组件的最佳实践。
Core Principles
核心原则
- Copy, Don't Install: Components are copied to your project, not installed as dependencies
- Customizable: Modify components directly in your codebase
- Accessible: Built on Radix UI primitives with ARIA support
- Type-Safe: Full TypeScript support
- Composable: Build complex UIs from simple primitives
- 复制而非安装:将组件复制到你的项目中,而非作为依赖安装
- 可定制:直接在你的代码库中修改组件
- 无障碍支持:基于Radix UI基础组件构建,支持ARIA规范
- 类型安全:完整支持TypeScript
- 可组合:通过简单基础组件构建复杂UI
Installation
安装步骤
Initial Setup
初始设置
bash
npx shadcn@latest initbash
npx shadcn@latest initAdd Components
添加组件
bash
undefinedbash
undefinedAdd individual components
添加单个组件
npx shadcn@latest add button
npx shadcn@latest add form
npx shadcn@latest add dialog
npx shadcn@latest add button
npx shadcn@latest add form
npx shadcn@latest add dialog
Add multiple
批量添加
npx shadcn@latest add button card dialog
undefinednpx shadcn@latest add button card dialog
undefinedTroubleshooting
故障排除
npm Cache Errors (ENOTEMPTY)
npm缓存错误(ENOTEMPTY)
If fails with npm cache errors like or :
npx shadcn@latest addENOTEMPTYsyscall renameSolution 1: Clear npm cache
bash
npm cache clean --force
npx shadcn@latest add tableSolution 2: Use pnpm (recommended)
bash
pnpm dlx shadcn@latest add tableSolution 3: Use yarn
bash
yarn dlx shadcn@latest add tableSolution 4: Manual component installation
Visit the shadcn/ui documentation for the specific component and copy the code directly into your project.
如果执行时出现或等npm缓存错误:
npx shadcn@latest addENOTEMPTYsyscall rename解决方案1:清理npm缓存
bash
npm cache clean --force
npx shadcn@latest add table解决方案2:使用pnpm(推荐)
bash
pnpm dlx shadcn@latest add table解决方案3:使用yarn
bash
yarn dlx shadcn@latest add table解决方案4:手动安装组件
访问shadcn/ui官方文档找到对应组件,直接将代码复制到你的项目中。
Component Usage
组件使用示例
Button & Card
按钮与卡片
typescript
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
// Variants
<Button>Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
// Card
<Card>
<CardHeader>
<CardTitle>{post.title}</CardTitle>
<CardDescription>{post.author}</CardDescription>
</CardHeader>
<CardContent>
<p>{post.excerpt}</p>
</CardContent>
</Card>typescript
import { Button } from '@/components/ui/button';
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card';
// 变体
<Button>默认按钮</Button>
<Button variant="destructive">危险按钮</Button>
<Button variant="outline">轮廓按钮</Button>
// 卡片
<Card>
<CardHeader>
<CardTitle>{post.title}</CardTitle>
<CardDescription>{post.author}</CardDescription>
</CardHeader>
<CardContent>
<p>{post.excerpt}</p>
</CardContent>
</Card>Dialog
对话框
typescript
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
export function CreatePostDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Create Post</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create New Post</DialogTitle>
<DialogDescription>
Fill in the details below to create a new post.
</DialogDescription>
</DialogHeader>
<PostForm />
</DialogContent>
</Dialog>
);
}typescript
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
export function CreatePostDialog() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>创建文章</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>创建新文章</DialogTitle>
<DialogDescription>
填写以下信息创建新文章。
</DialogDescription>
</DialogHeader>
<PostForm />
</DialogContent>
</Dialog>
);
}Forms
表单
Basic Form with react-hook-form
结合react-hook-form的基础表单
typescript
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as 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';
import { Textarea } from '@/components/ui/textarea';
const formSchema = z.object({
title: z.string().min(1, 'Title is required'),
content: z.string().min(10, 'Content must be at least 10 characters')
});
export function PostForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: '',
content: ''
}
});
const onSubmit = (values: z.infer<typeof formSchema>) => {
console.log(values);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="Post title" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>Content</FormLabel>
<FormControl>
<Textarea placeholder="Write your post..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Create Post</Button>
</form>
</Form>
);
}typescript
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as 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';
import { Textarea } from '@/components/ui/textarea';
const formSchema = z.object({
title: z.string().min(1, '标题为必填项'),
content: z.string().min(10, '内容至少需要10个字符')
});
export function PostForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: '',
content: ''
}
});
const onSubmit = (values: z.infer<typeof formSchema>) => {
console.log(values);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>标题</FormLabel>
<FormControl>
<Input placeholder="文章标题" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="content"
render={({ field }) => (
<FormItem>
<FormLabel>内容</FormLabel>
<FormControl>
<Textarea placeholder="撰写你的文章..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">创建文章</Button>
</form>
</Form>
);
}Select Field
选择字段
typescript
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem>
<FormLabel>Category</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tech">Technology</SelectItem>
<SelectItem value="design">Design</SelectItem>
<SelectItem value="business">Business</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>typescript
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem>
<FormLabel>分类</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="选择分类" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="tech">技术</SelectItem>
<SelectItem value="design">设计</SelectItem>
<SelectItem value="business">商业</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>Data Display
数据展示
Table
表格
typescript
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
export function PostsTable({ posts }: { posts: Post[] }) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Author</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{posts.map((post) => (
<TableRow key={post.id}>
<TableCell className="font-medium">{post.title}</TableCell>
<TableCell>{post.author.name}</TableCell>
<TableCell>
<Badge variant={post.published ? 'default' : 'secondary'}>
{post.published ? 'Published' : 'Draft'}
</Badge>
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">Edit</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}typescript
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
export function PostsTable({ posts }: { posts: Post[] }) {
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>标题</TableHead>
<TableHead>作者</TableHead>
<TableHead>状态</TableHead>
<TableHead className="text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{posts.map((post) => (
<TableRow key={post.id}>
<TableCell className="font-medium">{post.title}</TableCell>
<TableCell>{post.author.name}</TableCell>
<TableCell>
<Badge variant={post.published ? 'default' : 'secondary'}>
{post.published ? '已发布' : '草稿'}
</Badge>
</TableCell>
<TableCell className="text-right">
<Button variant="ghost" size="sm">编辑</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}Navigation
导航
Badge & Dropdown Menu
徽章与下拉菜单
typescript
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
export function UserMenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost">
<User className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Log out</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}typescript
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
export function UserMenu() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost">
<User className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>我的账户</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>个人资料</DropdownMenuItem>
<DropdownMenuItem>设置</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>退出登录</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}Tabs
标签页
typescript
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
export function PostTabs() {
return (
<Tabs defaultValue="published">
<TabsList>
<TabsTrigger value="published">Published</TabsTrigger>
<TabsTrigger value="drafts">Drafts</TabsTrigger>
<TabsTrigger value="archived">Archived</TabsTrigger>
</TabsList>
<TabsContent value="published">
<PublishedPosts />
</TabsContent>
<TabsContent value="drafts">
<DraftPosts />
</TabsContent>
<TabsContent value="archived">
<ArchivedPosts />
</TabsContent>
</Tabs>
);
}typescript
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
export function PostTabs() {
return (
<Tabs defaultValue="published">
<TabsList>
<TabsTrigger value="published">已发布</TabsTrigger>
<TabsTrigger value="drafts">草稿</TabsTrigger>
<TabsTrigger value="archived">已归档</TabsTrigger>
</TabsList>
<TabsContent value="published">
<PublishedPosts />
</TabsContent>
<TabsContent value="drafts">
<DraftPosts />
</TabsContent>
<TabsContent value="archived">
<ArchivedPosts />
</TabsContent>
</Tabs>
);
}Feedback
反馈组件
Toast
提示框(Toast)
typescript
'use client';
import { useToast } from '@/components/ui/use-toast';
import { Button } from '@/components/ui/button';
export function ToastExample() {
const { toast } = useToast();
return (
<Button
onClick={() => {
toast({
title: 'Post created',
description: 'Your post has been published successfully.'
});
}}
>
Create Post
</Button>
);
}
// With variant
toast({
variant: 'destructive',
title: 'Error',
description: 'Failed to create post. Please try again.'
});typescript
'use client';
import { useToast } from '@/components/ui/use-toast';
import { Button } from '@/components/ui/button';
export function ToastExample() {
const { toast } = useToast();
return (
<Button
onClick={() => {
toast({
title: '文章已创建',
description: '你的文章已成功发布。'
});
}}
>
创建文章
</Button>
);
}
// 带变体样式
toast({
variant: 'destructive',
title: '错误',
description: '创建文章失败,请重试。'
});Alert
警告框(Alert)
typescript
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { AlertCircle } from 'lucide-react';
export function AlertExample() {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>
Your session has expired. Please log in again.
</AlertDescription>
</Alert>
);
}typescript
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { AlertCircle } from 'lucide-react';
export function AlertExample() {
return (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>错误</AlertTitle>
<AlertDescription>
你的会话已过期,请重新登录。
</AlertDescription>
</Alert>
);
}Loading States
加载状态
Skeleton
骨架屏(Skeleton)
typescript
import { Skeleton } from '@/components/ui/skeleton';
export function PostCardSkeleton() {
return (
<div className="flex flex-col space-y-3">
<Skeleton className="h-[125px] w-full rounded-xl" />
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
);
}typescript
import { Skeleton } from '@/components/ui/skeleton';
export function PostCardSkeleton() {
return (
<div className="flex flex-col space-y-3">
<Skeleton className="h-[125px] w-full rounded-xl" />
<div className="space-y-2">
<Skeleton className="h-4 w-[250px]" />
<Skeleton className="h-4 w-[200px]" />
</div>
</div>
);
}Customization
定制化
Modifying Components
修改组件
Components are in your codebase - edit them directly:
typescript
// components/ui/button.tsx
export const buttonVariants = cva(
"inline-flex items-center justify-center...",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
// Add custom variant
brand: "bg-gradient-to-r from-blue-500 to-purple-600 text-white"
}
}
}
);组件已在你的代码库中,可直接编辑:
typescript
// components/ui/button.tsx
export const buttonVariants = cva(
"inline-flex items-center justify-center...",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
// 添加自定义变体
brand: "bg-gradient-to-r from-blue-500 to-purple-600 text-white"
}
}
}
);Using Custom Variant
使用自定义变体
typescript
<Button variant="brand">Custom Brand Button</Button>typescript
<Button variant="brand">自定义品牌按钮</Button>Theming
主题配置
CSS Variables (OKLCH Format)
CSS变量(OKLCH格式)
shadcn/ui now uses OKLCH color format for better color accuracy and perceptual uniformity:
css
/* app/globals.css */
@layer base {
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
/* ... */
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.598 0.15 264);
--primary-foreground: oklch(0.205 0 0);
/* ... */
}
}shadcn/ui 现在采用OKLCH颜色格式,以实现更准确的颜色和感知一致性:
css
/* app/globals.css */
@layer base {
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
/* ... */
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--primary: oklch(0.598 0.15 264);
--primary-foreground: oklch(0.205 0 0);
/* ... */
}
}Dark Mode
暗黑模式
typescript
// components/theme-toggle.tsx
'use client';
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-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
);
}typescript
// components/theme-toggle.tsx
'use client';
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-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">切换主题</span>
</Button>
);
}Composition Patterns
组合模式
Combining Components
组件组合
typescript
export function CreatePostCard() {
return (
<Card>
<CardHeader>
<CardTitle>Create Post</CardTitle>
<CardDescription>Share your thoughts with the world</CardDescription>
</CardHeader>
<CardContent>
<PostForm />
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">Save Draft</Button>
<Button>Publish</Button>
</CardFooter>
</Card>
);
}typescript
export function CreatePostCard() {
return (
<Card>
<CardHeader>
<CardTitle>创建文章</CardTitle>
<CardDescription>与世界分享你的想法</CardDescription>
</CardHeader>
<CardContent>
<PostForm />
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline">保存草稿</Button>
<Button>发布</Button>
</CardFooter>
</Card>
);
}Modal with Form
带表单的模态框
typescript
export function CreatePostModal() {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>New Post</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>Create Post</DialogTitle>
</DialogHeader>
<PostForm onSuccess={() => setOpen(false)} />
</DialogContent>
</Dialog>
);
}typescript
export function CreatePostModal() {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>新文章</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>创建文章</DialogTitle>
</DialogHeader>
<PostForm onSuccess={() => setOpen(false)} />
</DialogContent>
</Dialog>
);
}Additional Resources
额外资源
For detailed information, see:
- Component Catalog
- Form Patterns
- Theming Guide
如需详细信息,请查看:
- 组件目录
- 表单模式
- 主题指南