shadcn

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

shadcn/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

核心原则

  1. Copy, Don't Install: Components are copied to your project, not installed as dependencies
  2. Customizable: Modify components directly in your codebase
  3. Accessible: Built on Radix UI primitives with ARIA support
  4. Type-Safe: Full TypeScript support
  5. Composable: Build complex UIs from simple primitives
  1. 复制而非安装:将组件复制到你的项目中,而非作为依赖安装
  2. 可定制:直接在你的代码库中修改组件
  3. 无障碍支持:基于Radix UI基础组件构建,支持ARIA规范
  4. 类型安全:完整支持TypeScript
  5. 可组合:通过简单基础组件构建复杂UI

Installation

安装步骤

Initial Setup

初始设置

bash
npx shadcn@latest init
bash
npx shadcn@latest init

Add Components

添加组件

bash
undefined
bash
undefined

Add 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
undefined
npx shadcn@latest add button card dialog
undefined

Troubleshooting

故障排除

npm Cache Errors (ENOTEMPTY)

npm缓存错误(ENOTEMPTY)

If
npx shadcn@latest add
fails with npm cache errors like
ENOTEMPTY
or
syscall rename
:
Solution 1: Clear npm cache
bash
npm cache clean --force
npx shadcn@latest add table
Solution 2: Use pnpm (recommended)
bash
pnpm dlx shadcn@latest add table
Solution 3: Use yarn
bash
yarn dlx shadcn@latest add table
Solution 4: Manual component installation
Visit the shadcn/ui documentation for the specific component and copy the code directly into your project.
如果执行
npx shadcn@latest add
时出现
ENOTEMPTY
syscall rename
等npm缓存错误:
解决方案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
如需详细信息,请查看:
  • 组件目录
  • 表单模式
  • 主题指南