mcpField
S
shadcn-ui

shadcn-ui

github.com/existential-birds设计与 UI85352 次安装12 周前更新
代码仓库

关于 shadcn-ui

shadcn-ui 是面向组件开发者的技能,涵盖 CLI 配置与组件实现模式,包括变体、组合、样式和无障碍状态。

主要功能

  1. 01使用 CLI 初始化项目并添加组件
  2. 02应用 cn() 与 CVA 样式模式
  3. 03构建复合组件和多态组件
  4. 04使用 data-slot 属性与 CSS 选择器
  5. 05检查无障碍状态和深色模式样式

适用场景

  1. 01在应用中配置 shadcn/ui
  2. 02选择组件变体与组合模式
  3. 03通过 data-slot 选择器设置嵌套样式
  4. 04检查组件的无障碍状态

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/existential-birds/beagle --skill shadcn-ui

安装后在 Claude Code 中输入/shadcn-ui确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
components.md
cva.md
patterns.md

详细介绍

shadcn/ui 组件开发

目录

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 时跳过——例如,从此技能复制代码片段):

  1. 工作目录: 通过条件: pwd 和该路径下的 package.json 能确认这是应接收 components/ 和 components.json 的应用根目录(如果 shell 位于其他位置,请使用 -c <cwd>)。
  2. 执行 init 后: 通过条件: 该应用根目录下存在 components.json(或存在于单体仓库布局中有文档说明的路径下)。
  3. 执行 --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