shadcn-ui
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chineseshadcn/ui Component Development
shadcn/ui 组件开发
Contents
目录
- CLI Commands - Installing and adding components
- Quick Reference - cn(), basic CVA pattern
- Component Anatomy - Props typing, asChild, data-slot
- Component Patterns - Compound components
- Styling Techniques - CVA variants, modern CSS selectors, accessibility states
- Decision Tables - When to use CVA, compound components, asChild, Context
- Common Patterns - Form elements, dialogs, sidebars
- Reference Files - Full implementations and advanced patterns
CLI Commands
CLI 命令
Initialize shadcn/ui
初始化 shadcn/ui
bash
npx shadcn@latest initThis creates a configuration file and sets up:
components.json- 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
undefinedbash
undefinedAdd 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
常用选项
- - Skip confirmation prompt
-y, --yes - - Overwrite existing files
-o, --overwrite - - Set working directory
-c, --cwd <cwd> - - Use src directory structure
--src-dir
- - 跳过确认提示
-y, --yes - - 覆盖现有文件
-o, --overwrite - - 设置工作目录
-c, --cwd <cwd> - - 使用src目录结构
--src-dir
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-slottsx
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-slottsx
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 for CSS targeting:
data-slottsx
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]"
)}
/>每个组件都包含属性用于CSS定位:
data-slottsx
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 layoutGroup/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 onlytsx
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-backgroundtext-foregroundbg-primarytext-primary-foregroundbg-cardtext-card-foregroundborder-inputtext-muted-foreground语义化Token会自动适配暗黑模式:
tsx
className="bg-background text-foreground dark:bg-input/30 dark:hover:bg-input/50"常用Token:, , , , , , ,
bg-backgroundtext-foregroundbg-primarytext-primary-foregroundbg-cardtext-card-foregroundborder-inputtext-muted-foregroundDecision Tables
决策对照表
When to Use CVA
何时使用CVA
| Scenario | Use CVA | Alternative |
|---|---|---|
| Multiple visual variants (primary, outline, ghost) | Yes | Plain className |
| Size variations (sm, md, lg) | Yes | Plain className |
| Compound conditions (outline + large = thick border) | Yes | Conditional cn() |
| One-off custom styling | No | className prop |
| Dynamic colors from props | No | Inline styles or CSS variables |
| 场景 | 使用CVA | 替代方案 |
|---|---|---|
| 多视觉变体(primary、outline、ghost) | 是 | 原生className |
| 尺寸变体(sm、md、lg) | 是 | 原生className |
| 复合条件(outline + large = 粗边框) | 是 | 条件式cn() |
| 一次性自定义样式 | 否 | className属性 |
| 从Props传入动态颜色 | 否 | 内联样式或CSS变量 |
When to Use Compound Components
何时使用复合组件
| Scenario | Use Compound | Alternative |
|---|---|---|
| Complex UI with multiple semantic parts | Yes | Single component with many props |
| Optional sections (header, footer) | Yes | Boolean show/hide props |
| Different styling for each part | Yes | CSS selectors |
| Shared state between parts | Yes + Context | Props drilling |
| Simple wrapper with children | No | Single component |
| 场景 | 使用复合组件 | 替代方案 |
|---|---|---|
| 包含多个语义化部分的复杂UI | 是 | 带大量Props的单一组件 |
| 可选区域(头部、底部) | 是 | 布尔值控制显示/隐藏的Props |
| 各部分需要独立样式 | 是 | CSS选择器 |
| 各部分共享状态 | 是 + Context | Props透传 |
| 简单的包裹型组件 | 否 | 单一组件 |
When to Use asChild
何时使用asChild
| Scenario | Use asChild | Alternative |
|---|---|---|
| Component should work as link or button | Yes | Duplicate component |
| Need button styles on custom element | Yes | Export variant styles |
| Integration with routing libraries | Yes | Wrapper components |
| Always renders same element | No | Standard component |
| 场景 | 使用asChild | 替代方案 |
|---|---|---|
| 组件需要同时支持链接或按钮形态 | 是 | 重复实现组件 |
| 需要在自定义元素上应用按钮样式 | 是 | 导出变体样式 |
| 与路由库集成 | 是 | 包裹型组件 |
| 始终渲染相同元素 | 否 | 标准组件 |
When to Use Context
何时使用Context
| Scenario | Use Context | Alternative |
|---|---|---|
| Deep prop drilling (>3 levels) | Yes | Props |
| State shared by many siblings | Yes | Lift state up |
| Plugin/extension architecture | Yes | Props |
| Simple parent-child communication | No | Props |
| 场景 | 使用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()选择器