shadcn-ui

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

shadcn/ui Component Development

shadcn/ui 组件开发

Contents

目录

CLI Commands

CLI 命令

Initialize shadcn/ui

初始化 shadcn/ui

bash
npx shadcn@latest init
This creates a
components.json
configuration file and sets up:
  • Tailwind CSS configuration
  • CSS variables for theming
  • cn() utility function
  • Required dependencies
bash
npx shadcn@latest init
此命令会创建一个
components.json
配置文件并完成以下设置:
  • Tailwind CSS 配置
  • 用于主题定制的CSS变量
  • cn() 工具函数
  • 所需依赖项

Add Components

添加组件

bash
undefined
bash
undefined

Add a single component

添加单个组件

npx shadcn@latest add button
npx shadcn@latest add button

Add multiple components

添加多个组件

npx shadcn@latest add button card dialog
npx shadcn@latest add button card dialog

Add all available components

添加所有可用组件

npx shadcn@latest add --all

**Important:** The package name changed in 2024:
- Old (deprecated): `npx shadcn-ui@latest add`
- Current: `npx shadcn@latest add`
npx shadcn@latest add --all

**注意:** 2024年包名已变更:
- 旧版(已废弃):`npx shadcn-ui@latest add`
- 当前版本:`npx shadcn@latest add`

Common Options

常用选项

  • -y, --yes
    - Skip confirmation prompt
  • -o, --overwrite
    - Overwrite existing files
  • -c, --cwd <cwd>
    - Set working directory
  • --src-dir
    - Use src directory structure
  • -y, --yes
    - 跳过确认提示
  • -o, --overwrite
    - 覆盖现有文件
  • -c, --cwd <cwd>
    - 设置工作目录
  • --src-dir
    - 使用src目录结构

Quick Reference

快速参考

cn() Utility

cn() 工具函数

tsx
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}
tsx
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

Basic CVA Pattern

基础CVA模式

tsx
import { cva, type VariantProps } from "class-variance-authority"

const buttonVariants = cva(
  "base-classes-applied-to-all-variants",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground",
        outline: "border bg-background",
      },
      size: {
        sm: "h-8 px-3",
        lg: "h-10 px-6",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "sm",
    },
  }
)

function Button({
  variant,
  size,
  className,
  ...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>) {
  return (
    <button
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  )
}

export { Button, buttonVariants }
tsx
import { cva, type VariantProps } from "class-variance-authority"

const buttonVariants = cva(
  "base-classes-applied-to-all-variants",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground",
        outline: "border bg-background",
      },
      size: {
        sm: "h-8 px-3",
        lg: "h-10 px-6",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "sm",
    },
  }
)

function Button({
  variant,
  size,
  className,
  ...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants>) {
  return (
    <button
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  )
}

export { Button, buttonVariants }

Component Anatomy

组件结构

Props Typing Patterns

Props类型定义模式

tsx
// HTML elements
function Component({ className, ...props }: React.ComponentProps<"div">) {
  return <div className={cn("base-classes", className)} {...props} />
}

// Radix primitives
function Component({ className, ...props }: React.ComponentProps<typeof RadixPrimitive.Root>) {
  return <RadixPrimitive.Root className={cn("base-classes", className)} {...props} />
}

// With CVA variants
function Component({
  variant, size, className, ...props
}: React.ComponentProps<"button"> & VariantProps<typeof variants>) {
  return <button className={cn(variants({ variant, size }), className)} {...props} />
}
tsx
// HTML元素
function Component({ className, ...props }: React.ComponentProps<"div">) {
  return <div className={cn("base-classes", className)} {...props} />
}

// Radix原语
function Component({ className, ...props }: React.ComponentProps<typeof RadixPrimitive.Root>) {
  return <RadixPrimitive.Root className={cn("base-classes", className)} {...props} />
}

// 结合CVA变体
function Component({
  variant, size, className, ...props
}: React.ComponentProps<"button"> & VariantProps<typeof variants>) {
  return <button className={cn(variants({ variant, size }), className)} {...props} />
}

asChild Pattern

asChild 模式

Enables polymorphic rendering via
@radix-ui/react-slot
:
tsx
import { Slot } from "@radix-ui/react-slot"

function Button({
  asChild = false,
  className,
  variant,
  size,
  ...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
  const Comp = asChild ? Slot : "button"
  return (
    <Comp
      data-slot="button"
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  )
}
Usage:
tsx
<Button>Click me</Button>                           // Renders <button>
<Button asChild><a href="/home">Home</a></Button>   // Renders <a> with button styling
<Button asChild><Link href="/dash">Dash</Link></Button>  // Works with Next.js Link
通过
@radix-ui/react-slot
实现多态渲染:
tsx
import { Slot } from "@radix-ui/react-slot"

function Button({
  asChild = false,
  className,
  variant,
  size,
  ...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & { asChild?: boolean }) {
  const Comp = asChild ? Slot : "button"
  return (
    <Comp
      data-slot="button"
      className={cn(buttonVariants({ variant, size }), className)}
      {...props}
    />
  )
}
使用示例:
tsx
<Button>Click me</Button>                           // 渲染为<button>
<Button asChild><a href="/home">Home</a></Button>   // 渲染为带按钮样式的<a>
<Button asChild><Link href="/dash">Dash</Link></Button>  // 与Next.js Link兼容

data-slot Attributes

data-slot 属性

Every component includes
data-slot
for CSS targeting:
tsx
function Card({ ...props }) { return <div data-slot="card" {...props} /> }
function CardHeader({ ...props }) { return <div data-slot="card-header" {...props} /> }
CSS/Tailwind targeting:
css
[data-slot="button"] { /* styles */ }
[data-slot="card"] [data-slot="button"] { /* nested targeting */ }
tsx
<div className="[&_[data-slot=button]]:shadow-lg">
  <Button>Automatically styled</Button>
</div>
Conditional layouts with has():
tsx
<div
  data-slot="card-header"
  className={cn(
    "grid gap-2",
    "has-data-[slot=card-action]:grid-cols-[1fr_auto]"
  )}
/>
每个组件都包含
data-slot
属性用于CSS定位:
tsx
function Card({ ...props }) { return <div data-slot="card" {...props} /> }
function CardHeader({ ...props }) { return <div data-slot="card-header" {...props} /> }
CSS/Tailwind定位:
css
[data-slot="button"] { /* styles */ }
[data-slot="card"] [data-slot="button"] { /* nested targeting */ }
tsx
<div className="[&_[data-slot=button]]:shadow-lg">
  <Button>Automatically styled</Button>
</div>
使用has()实现条件布局:
tsx
<div
  data-slot="card-header"
  className={cn(
    "grid gap-2",
    "has-data-[slot=card-action]:grid-cols-[1fr_auto]"
  )}
/>

Component Patterns

组件模式

Compound Components

复合组件

tsx
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }

function Card({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card"
      className={cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className)}
      {...props}
    />
  )
}

function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="card-header" className={cn("grid gap-2 px-6", className)} {...props} />
}

function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />
}
tsx
export { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter }

function Card({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="card"
      className={cn("bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", className)}
      {...props}
    />
  )
}

function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="card-header" className={cn("grid gap-2 px-6", className)} {...props} />
}

function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
  return <div data-slot="card-title" className={cn("leading-none font-semibold", className)} {...props} />
}

Styling Techniques

样式技巧

CVA Variants

CVA 变体

Multiple dimensions:
tsx
const buttonVariants = cva("base-classes", {
  variants: {
    variant: {
      default: "bg-primary text-primary-foreground",
      destructive: "bg-destructive text-white",
      outline: "border bg-background",
      ghost: "hover:bg-accent",
      link: "text-primary underline-offset-4 hover:underline",
    },
    size: {
      default: "h-9 px-4 py-2",
      sm: "h-8 px-3",
      lg: "h-10 px-6",
      icon: "size-9",
    },
  },
  defaultVariants: { variant: "default", size: "default" },
})
Compound variants:
tsx
compoundVariants: [
  { variant: "outline", size: "lg", class: "border-2" },
]
Type extraction:
tsx
type ButtonVariants = VariantProps<typeof buttonVariants>
// Result: { variant?: "default" | "outline" | ..., size?: "sm" | "lg" | ... }
多维度变体:
tsx
const buttonVariants = cva("base-classes", {
  variants: {
    variant: {
      default: "bg-primary text-primary-foreground",
      destructive: "bg-destructive text-white",
      outline: "border bg-background",
      ghost: "hover:bg-accent",
      link: "text-primary underline-offset-4 hover:underline",
    },
    size: {
      default: "h-9 px-4 py-2",
      sm: "h-8 px-3",
      lg: "h-10 px-6",
      icon: "size-9",
    },
  },
  defaultVariants: { variant: "default", size: "default" },
})
复合变体:
tsx
compoundVariants: [
  { variant: "outline", size: "lg", class: "border-2" },
]
类型提取:
tsx
type ButtonVariants = VariantProps<typeof buttonVariants>
// 结果:{ variant?: "default" | "outline" | ..., size?: "sm" | "lg" | ... }

Modern CSS Selectors in Tailwind

Tailwind 中的现代CSS选择器

has() selector:
tsx
<button className="px-4 has-[>svg]:px-3">  // Adjusts padding when contains icon
<div className="has-data-[slot=action]:grid-cols-[1fr_auto]">  // Conditional layout
Group/peer selectors:
tsx
<div className="group" data-state="collapsed">
  <div className="group-data-[state=collapsed]:hidden">Hidden when collapsed</div>
</div>

<button className="peer/menu" data-active="true">Menu</button>
<div className="peer-data-[active=true]/menu:text-accent">Styled when sibling active</div>
Container queries:
tsx
<div className="@container/card">
  <div className="@md:flex-row">Responds to container width</div>
</div>
has() 选择器:
tsx
<button className="px-4 has-[>svg]:px-3">  // 当包含图标时调整内边距
<div className="has-data-[slot=action]:grid-cols-[1fr_auto]">  // 条件布局
Group/Peer 选择器:
tsx
<div className="group" data-state="collapsed">
  <div className="group-data-[state=collapsed]:hidden">折叠时隐藏</div>
</div>

<button className="peer/menu" data-active="true">Menu</button>
<div className="peer-data-[active=true]/menu:text-accent">兄弟元素激活时样式变更</div>
容器查询:
tsx
<div className="@container/card">
  <div className="@md:flex-row">根据容器宽度响应式布局</div>
</div>

Accessibility States

无障碍访问状态

tsx
className={cn(
  // Focus
  "outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
  // Invalid
  "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
  // Disabled
  "disabled:pointer-events-none disabled:opacity-50",
)}

<span className="sr-only">Close</span>  // Screen reader only
tsx
className={cn(
  // 聚焦状态
  "outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
  // 无效状态
  "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
  // 禁用状态
  "disabled:pointer-events-none disabled:opacity-50",
)}

<span className="sr-only">Close</span>  // 仅屏幕阅读器可见

Dark Mode

暗黑模式

Semantic tokens adapt automatically:
tsx
className="bg-background text-foreground dark:bg-input/30 dark:hover:bg-input/50"
Tokens:
bg-background
,
text-foreground
,
bg-primary
,
text-primary-foreground
,
bg-card
,
text-card-foreground
,
border-input
,
text-muted-foreground
语义化Token会自动适配暗黑模式:
tsx
className="bg-background text-foreground dark:bg-input/30 dark:hover:bg-input/50"
常用Token:
bg-background
,
text-foreground
,
bg-primary
,
text-primary-foreground
,
bg-card
,
text-card-foreground
,
border-input
,
text-muted-foreground

Decision Tables

决策对照表

When to Use CVA

何时使用CVA

ScenarioUse CVAAlternative
Multiple visual variants (primary, outline, ghost)YesPlain className
Size variations (sm, md, lg)YesPlain className
Compound conditions (outline + large = thick border)YesConditional cn()
One-off custom stylingNoclassName prop
Dynamic colors from propsNoInline styles or CSS variables
场景使用CVA替代方案
多视觉变体(primary、outline、ghost)原生className
尺寸变体(sm、md、lg)原生className
复合条件(outline + large = 粗边框)条件式cn()
一次性自定义样式className属性
从Props传入动态颜色内联样式或CSS变量

When to Use Compound Components

何时使用复合组件

ScenarioUse CompoundAlternative
Complex UI with multiple semantic partsYesSingle component with many props
Optional sections (header, footer)YesBoolean show/hide props
Different styling for each partYesCSS selectors
Shared state between partsYes + ContextProps drilling
Simple wrapper with childrenNoSingle component
场景使用复合组件替代方案
包含多个语义化部分的复杂UI带大量Props的单一组件
可选区域(头部、底部)布尔值控制显示/隐藏的Props
各部分需要独立样式CSS选择器
各部分共享状态是 + ContextProps透传
简单的包裹型组件单一组件

When to Use asChild

何时使用asChild

ScenarioUse asChildAlternative
Component should work as link or buttonYesDuplicate component
Need button styles on custom elementYesExport variant styles
Integration with routing librariesYesWrapper components
Always renders same elementNoStandard component
场景使用asChild替代方案
组件需要同时支持链接或按钮形态重复实现组件
需要在自定义元素上应用按钮样式导出变体样式
与路由库集成包裹型组件
始终渲染相同元素标准组件

When to Use Context

何时使用Context

ScenarioUse ContextAlternative
Deep prop drilling (>3 levels)YesProps
State shared by many siblingsYesLift state up
Plugin/extension architectureYesProps
Simple parent-child communicationNoProps
场景使用Context替代方案
深层Props透传(>3层)Props透传
多个兄弟组件共享状态状态提升
插件/扩展架构Props透传
简单的父子组件通信Props透传

Common Patterns

常见模式

Form Input

表单输入框

tsx
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
  return (
    <input
      type={type}
      data-slot="input"
      className={cn(
        "h-9 w-full rounded-md border px-3 py-1",
        "outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
        "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
        "disabled:cursor-not-allowed disabled:opacity-50",
        "placeholder:text-muted-foreground dark:bg-input/30",
        className
      )}
      {...props}
    />
  )
}
tsx
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
  return (
    <input
      type={type}
      data-slot="input"
      className={cn(
        "h-9 w-full rounded-md border px-3 py-1",
        "outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
        "aria-invalid:border-destructive aria-invalid:ring-destructive/20",
        "disabled:cursor-not-allowed disabled:opacity-50",
        "placeholder:text-muted-foreground dark:bg-input/30",
        className
      )}
      {...props}
    />
  )
}

Dialog Content

对话框内容

tsx
function DialogContent({ children, showCloseButton = true, ...props }) {
  return (
    <DialogPortal>
      <DialogOverlay />
      <DialogPrimitive.Content
        data-slot="dialog-content"
        className={cn(
          "fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] w-full max-w-lg",
          "bg-background border rounded-lg p-6 shadow-lg",
          "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
          "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
        )}
        {...props}
      >
        {children}
        {showCloseButton && (
          <DialogPrimitive.Close className="absolute top-4 right-4">
            <XIcon /><span className="sr-only">Close</span>
          </DialogPrimitive.Close>
        )}
      </DialogPrimitive.Content>
    </DialogPortal>
  )
}
tsx
function DialogContent({ children, showCloseButton = true, ...props }) {
  return (
    <DialogPortal>
      <DialogOverlay />
      <DialogPrimitive.Content
        data-slot="dialog-content"
        className={cn(
          "fixed top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] w-full max-w-lg",
          "bg-background border rounded-lg p-6 shadow-lg",
          "data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
          "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
        )}
        {...props}
      >
        {children}
        {showCloseButton && (
          <DialogPrimitive.Close className="absolute top-4 right-4">
            <XIcon /><span className="sr-only">Close</span>
          </DialogPrimitive.Close>
        )}
      </DialogPrimitive.Content>
    </DialogPortal>
  )
}

Sidebar with Context

带Context的侧边栏

tsx
function SidebarProvider({ defaultOpen = true, children }) {
  const isMobile = useIsMobile()
  const [open, setOpen] = React.useState(defaultOpen)

  React.useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === "b" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault()
        setOpen(o => !o)
      }
    }
    window.addEventListener("keydown", handleKeyDown)
    return () => window.removeEventListener("keydown", handleKeyDown)
  }, [])

  const contextValue = React.useMemo(
    () => ({ state: open ? "expanded" : "collapsed", open, setOpen, isMobile }),
    [open, setOpen, isMobile]
  )

  return (
    <SidebarContext.Provider value={contextValue}>
      <div
        data-slot="sidebar-wrapper"
        style={{ "--sidebar-width": "16rem", "--sidebar-width-icon": "3rem" } as React.CSSProperties}
      >
        {children}
      </div>
    </SidebarContext.Provider>
  )
}
tsx
function SidebarProvider({ defaultOpen = true, children }) {
  const isMobile = useIsMobile()
  const [open, setOpen] = React.useState(defaultOpen)

  React.useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === "b" && (e.metaKey || e.ctrlKey)) {
        e.preventDefault()
        setOpen(o => !o)
      }
    }
    window.addEventListener("keydown", handleKeyDown)
    return () => window.removeEventListener("keydown", handleKeyDown)
  }, [])

  const contextValue = React.useMemo(
    () => ({ state: open ? "expanded" : "collapsed", open, setOpen, isMobile }),
    [open, setOpen, isMobile]
  )

  return (
    <SidebarContext.Provider value={contextValue}>
      <div
        data-slot="sidebar-wrapper"
        style={{ "--sidebar-width": "16rem", "--sidebar-width-icon": "3rem" } as React.CSSProperties}
      >
        {children}
      </div>
    </SidebarContext.Provider>
  )
}

Reference Files

参考文件

For comprehensive examples and advanced patterns:
  • components.md - Full implementations: Button, Card, Badge, Input, Label, Textarea, Dialog
  • cva.md - CVA patterns: compound variants, responsive variants, type extraction
  • patterns.md - Architectural patterns: compound components, asChild, controlled state, Context, data-slot, has() selectors
如需完整示例和高级模式,请参考:
  • components.md - 完整实现示例:Button、Card、Badge、Input、Label、Textarea、Dialog
  • cva.md - CVA模式:复合变体、响应式变体、类型提取
  • patterns.md - 架构模式:复合组件、asChild、受控状态、Context、data-slot、has()选择器