关于 shadcn-ui
shadcn-ui 是面向组件开发者的技能,涵盖 CLI 配置与组件实现模式,包括变体、组合、样式和无障碍状态。
主要功能
- 01使用 CLI 初始化项目并添加组件
- 02应用 cn() 与 CVA 样式模式
- 03构建复合组件和多态组件
- 04使用 data-slot 属性与 CSS 选择器
- 05检查无障碍状态和深色模式样式
适用场景
- 01在应用中配置 shadcn/ui
- 02选择组件变体与组合模式
- 03通过 data-slot 选择器设置嵌套样式
- 04检查组件的无障碍状态
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ existential-birds/ beagle --skill shadcn-ui安装后在 Claude Code 中输入/shadcn-ui确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/components.mdcva.mdpatterns.md详细介绍
shadcn/ui 组件开发
目录
- CLI 命令 - 安装和添加组件
- 快速参考 - cn()、基本 CVA 模式
- 组件剖析 - Props 类型、asChild、data-slot
- 组件模式 - 复合组件
- 样式技巧 - CVA 变体、现代 CSS 选择器、无障碍状态
- 决策表 - 何时使用 CVA、复合组件、asChild、上下文
- 常见模式 - 表单元素、对话框、侧边栏
- 参考文件 - 完整实现和高级模式
CLI 命令
初始化 shadcn/ui
npx shadcn@latest init
这会创建一个 components.json 配置文件,并设置:
- Tailwind CSS 配置
- 用于主题的 CSS 变量
- cn() 工具函数
- 所需依赖项
添加组件
# Add a single component
npx shadcn@latest add button
# Add multiple components
npx shadcn@latest add button card dialog
# Add all available components
npx shadcn@latest add --all
重要: 包名于 2024 年更改:
- 旧版(已弃用):
npx shadcn-ui@latest add - 当前版本:
npx shadcn@latest add
常用选项
-y, --yes- 跳过确认提示-o, --overwrite- 覆盖现有文件-c, --cwd <cwd>- 设置工作目录--src-dir- 使用 src 目录结构
门槛检查(CLI 和文件更改)
在执行 init / add 之前,按顺序运行以下检查(仅在不运行 CLI 时跳过——例如,从此技能复制代码片段):
- 工作目录: 通过条件:
pwd和该路径下的package.json能确认这是应接收components/和components.json的应用根目录(如果 shell 位于其他位置,请使用-c <cwd>)。 - 执行
init后: 通过条件: 该应用根目录下存在components.json(或存在于单体仓库布局中有文档说明的路径下)。 - 执行
--overwrite前: 通过条件: 能列出将被替换的已跟踪文件,或版本控制记录显示此次更改是有意且可恢复的。
快速参考
cn() 工具函数
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
基本 CVA 模式
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 }
组件剖析
Props 类型模式
// 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} />
}
asChild 模式
通过 @radix-ui/react-slot 实现多态渲染:
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}
/>
)
}
用法:
<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
data-slot 属性
每个组件都包含用于 CSS 定位的 data-slot:
function Card({ ...props }) { return <div data-slot="card" {...props} /> }
function CardHeader({ ...props }) { return <div data-slot="card-header" {...props} /> }
CSS/Tailwind 定位:
[data-slot="button"] { /* styles */ }
[data-slot="card"] [data-slot="button"] { /* nested targeting */ }
<div className="[&_[data-slot=button]]:shadow-lg">
<Button>Automatically styled</Button>
</div>
使用 has() 的条件布局:
<div
data-slot="card-header"
className={cn(
"grid gap-2",
"has-data-[slot=card-action]:grid-cols-[1fr_auto]"
)}
/>
组件模式
复合组件
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} />
}
样式技巧
CVA 变体
多个维度:
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" },
})
复合变体:
compoundVariants: [
{ variant: "outline", size: "lg", class: "border-2" },
]
类型提取:
type ButtonVariants = VariantProps<typeof buttonVariants>
// Result: { variant?: "default" | "outline" | ..., size?: "sm" | "lg" | ... }
Tailwind 中的现代 CSS 选择器
has() 选择器:
<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 选择器:
<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>
容器查询:
<div className="@container/card">
<div className="@md:flex-row">Responds to container width</div>
</div>
无障碍状态
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
暗色模式
语义化令牌会自动适配:
className="bg-background text-foreground dark:bg-input/30 dark:hover:bg-input/50"
令牌:bg-background、text-foreground、bg-primary、text-primary-foreground、bg-card、text-card-foreground、border-input、text-muted-foreground
决策表
何时使用 CVA
| 场景 | 使用 CVA | 替代方案 |
|---|---|---|
| 多种视觉变体(primary、outline、ghost) | 是 | 普通 className |
| 尺寸变化(sm、md、lg) | 是 | 普通 className |
| 复合条件(outline + large = 粗边框) | 是 | 条件 cn() |
| 一次性自定义样式 | 否 | className 属性 |
| 来自 props 的动态颜色 | 否 | 内联样式或 CSS 变量 |
何时使用复合组件
| 场景 | 使用复合组件 | 替代方案 |
|---|---|---|
| 包含多个语义部分的复杂 UI | 是 | 带多个 props 的单个组件 |
| 可选区域(页眉、页脚) | 是 | 布尔型显示/隐藏 props |
| 每个部分的样式各不相同 | 是 | CSS 选择器 |
| 各部分共享状态 | 是 + 上下文 | Props 层层传递 |
| 带有 children 的简单包装器 | 否 | 单个组件 |
何时使用 asChild
| 场景 | 使用 asChild | 替代方案 |
|---|---|---|
| 组件应同时支持链接和按钮 | 是 | 复制组件 |
| 需要在自定义元素上使用按钮样式 | 是 | 导出变体样式 |
| 与路由库集成 | 是 | 包装组件 |
| 始终渲染相同的元素 | 否 | 标准组件 |
何时使用 Context
| 场景 | 使用 Context | 替代方案 |
|---|---|---|
| 深层属性逐层传递(>3 层) | 是 | 属性 |
| 多个同级组件共享状态 | 是 | 提升状态 |
| 插件/扩展架构 | 是 | 属性 |
| 简单的父子组件通信 | 否 | 属性 |
常见模式
表单输入
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}
/>
)
}
对话框内容
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>
)
}
使用 Context 的侧边栏
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>
)
}
参考文件
如需完整示例和高级模式,请参阅:
- components.md - 完整实现:Button、Card、Badge、Input、Label、Textarea、Dialog
- cva.md - CVA 模式:复合变体、响应式变体、类型提取
- patterns.md - 架构模式:复合组件、asChild、受控状态、Context、data-slot、has() 选择器
评论
支持 Markdown