mcpField
S
shadcn-ui

shadcn-ui

github.com/existential-birdsデザイン・UI85352 回インストール12 週間前に更新
リポジトリ

shadcn-ui について

shadcn/ui のコンポーネントを開発・カスタマイズする開発者向けのスキルです。CLI の設定から、バリアント、合成、スタイル、アクセシビリティの実装パターンまで扱います。

主な機能

  1. 01CLI による初期化とコンポーネント追加
  2. 02cn() と CVA のスタイルパターン
  3. 03複合コンポーネントと多態的なコンポーネント
  4. 04data-slot 属性と CSS セレクターの活用
  5. 05アクセシビリティ状態とダークモードの確認

活用シーン

  1. 01アプリで shadcn/ui を初期設定
  2. 02バリアントや合成のパターン選択
  3. 03data-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 を配置するアプリのルートを特定できること(シェルが別の場所にある場合は -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 prop
props からの動的な色いいえインラインスタイルまたは CSS 変数
複合コンポーネントを使う場面
シナリオ複合コンポーネントを使用代替案
複数の意味的なパーツを含む複雑な UIはい多数の props を持つ単一コンポーネント
任意のセクション(ヘッダー、フッター)はい表示/非表示を切り替える Boolean props
パーツごとに異なるスタイリングはいCSS セレクター
パーツ間で状態を共有するはい + Contextprops のバケツリレー
children を含むシンプルなラッパーいいえ単一コンポーネント
asChild を使用する場合
シナリオasChild を使用代替案
コンポーネントをリンクまたはボタンとして機能させるはいコンポーネントを複製する
カスタム要素にボタンのスタイルを適用する必要があるはいバリアントスタイルをエクスポートする
ルーティングライブラリと統合するはいラッパーコンポーネント
常に同じ要素をレンダリングするいいえ標準コンポーネント
Context を使用する場合
シナリオContext を使用代替案
深い階層での props のバケツリレー(3 階層超)はいProps
多くの兄弟コンポーネントで状態を共有するはい状態を上位に移動する
プラグイン/拡張機能のアーキテクチャはいProps
単純な親子間の通信いいえProps

よく使われるパターン

フォーム入力
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() セレクター

コメント