shadcn-ui について
shadcn/ui のコンポーネントを開発・カスタマイズする開発者向けのスキルです。CLI の設定から、バリアント、合成、スタイル、アクセシビリティの実装パターンまで扱います。
主な機能
- 01CLI による初期化とコンポーネント追加
- 02cn() と CVA のスタイルパターン
- 03複合コンポーネントと多態的なコンポーネント
- 04data-slot 属性と CSS セレクターの活用
- 05アクセシビリティ状態とダークモードの確認
活用シーン
- 01アプリで shadcn/ui を初期設定
- 02バリアントや合成のパターン選択
- 03data-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、Context を使う場面
- 一般的なパターン - フォーム要素、ダイアログ、サイドバー
- 参考ファイル - 完全な実装と高度なパターン
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を配置するアプリのルートを特定できること(シェルが別の場所にある場合は-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 prop |
| props からの動的な色 | いいえ | インラインスタイルまたは CSS 変数 |
複合コンポーネントを使う場面
| シナリオ | 複合コンポーネントを使用 | 代替案 |
|---|---|---|
| 複数の意味的なパーツを含む複雑な UI | はい | 多数の props を持つ単一コンポーネント |
| 任意のセクション(ヘッダー、フッター) | はい | 表示/非表示を切り替える Boolean props |
| パーツごとに異なるスタイリング | はい | CSS セレクター |
| パーツ間で状態を共有する | はい + Context | props のバケツリレー |
| 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() セレクター
コメント
Markdown 対応