shadcn-ui について
shadcn-uiは、ReactとTailwind CSSのプロジェクト向け再利用可能なコンポーネント集です。CLIでコードをプロジェクトに追加するため、開発者がコードを所有し、直接カスタマイズできます。
主な機能
- 01Radix UIとTailwind CSSを基盤に採用
- 02CLIでコンポーネントのソースを追加
- 03コードとスタイルをカスタマイズ可能
- 04フォーム、ダイアログ、テーブルを収録
- 05テーマ設定とダークモードに対応
- 06キーボード操作とARIAに対応
活用シーン
- 01Reactアプリのインターフェース構築
- 02プロジェクトに合わせたスタイル変更
- 03フォーム、ダイアログ、ナビゲーションの構成
- 04インターフェースへのダークテーマ設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ bobmatnyc/ claude-mpm-skills --skill shadcn-uiインストール後、Claude Code で/shadcn-uiを入力して読み込まれたか確認します。
同梱ファイルmetadata.json
詳細
shadcn/ui - コンポーネントライブラリ
progressive_disclosure: entry_point: summary, when_to_use, quick_start estimated_tokens: entry: 85 full: 4800
概要
shadcn/ui は、Radix UI のプリミティブと Tailwind CSS を使って構築された、再利用可能な React コンポーネントのコレクションです。従来のコンポーネントライブラリとは異なり、shadcn/ui のコンポーネントはプロジェクトに直接コピーするため、コードを完全に所有し、自由にカスタマイズできます。コンポーネントはアクセシブルで、カスタマイズ可能、かつオープンソースです。
基本理念:npm パッケージではなく、コンポーネントをコピー&ペーストします。コードは自分で所有します。
使用する場面
shadcn/ui を使用する場面:
- Tailwind CSS を使った React アプリケーションを構築する場合
- アクセシブルで、本番環境ですぐに使える UI コンポーネントが必要な場合
- コンポーネントのコードやスタイルを完全に制御したい場合
- 設定よりコンポジションを重視する場合
- Next.js、Vite、Remix、Astro を使って構築する場合
- ダークモードを最初からサポートする必要がある場合
- TypeScript を優先したコンポーネントが必要な場合
使用しない場面:
- Tailwind CSS を使用していない場合(スタイル設定の中核となる依存関係です)
- 古いブラウザーのサポートが必要な場合(最新の CSS 機能を使用します)
- コードの所有より、パッケージ化された npm ライブラリを好む場合
- React 以外のフレームワーク(Vue、Svelte、Angular)を使って構築する場合
クイックスタート
インストール
# Initialize shadcn/ui in your project
npx shadcn-ui@latest init
# Follow interactive prompts:
# - TypeScript? (yes/no)
# - Style: Default/New York
# - Base color: Slate/Gray/Zinc/Neutral/Stone
# - CSS variables: (yes/no)
# - React Server Components: (yes/no)
# - components.json location
# - Tailwind config location
# - CSS file location
# - Import alias (@/components)
最初のコンポーネントを追加する
# Add individual components
npx shadcn-ui@latest add button
npx shadcn-ui@latest add card
npx shadcn-ui@latest add dialog
# Add multiple components at once
npx shadcn-ui@latest add button card dialog form input
基本的な使い方
import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
export default function Example() {
return (
<Card>
<CardHeader>
<CardTitle>Welcome</CardTitle>
</CardHeader>
<CardContent>
<Button>Click me</Button>
</CardContent>
</Card>
)
}
アーキテクチャ
コピー&ペーストの理念
従来のライブラリとの主な違い:
- 従来型:
npm install component-library→ パッケージのバージョンに固定されます - shadcn/ui:コンポーネントを
components/ui/にコピー → コードを自分で所有します
利点:
- 完全なカスタマイズ制御
- パッケージの更新による破壊的変更がありません
- 特定のニーズに合わせて簡単に変更できます
- 実装が透明です
- デフォルトでツリーシェイキングに対応しています
コンポーネントの構造
src/
├── components/
│ └── ui/
│ ├── button.tsx # Component implementation
│ ├── card.tsx # Owns its code
│ ├── dialog.tsx # Modifiable
│ └── ...
├── lib/
│ └── utils.ts # cn() helper for class merging
└── app/
└── globals.css # Tailwind directives + CSS variables
技術スタック
中核となる依存関係:
- Radix UI:アクセシブルなコンポーネントのプリミティブ(ヘッドレス UI)
- Tailwind CSS:ユーティリティファーストのスタイリング
- TypeScript:型安全性
- class-variance-authority (CVA):バリアント管理
- clsx:クラス名の連結
- tailwind-merge:競合のないクラスのマージ
Radix UI との統合:
// shadcn/ui components wrap Radix primitives
import * as DialogPrimitive from "@radix-ui/react-dialog"
// Add styling and variants
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogContent = React.forwardRef<...>(
({ className, children, ...props }, ref) => (
<DialogPrimitive.Content
ref={ref}
className={cn("fixed ...", className)}
{...props}
/>
)
)
設定
components.json
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
}
}
主なオプション:
style:"default" または "new-york"(デザインバリアント)rsc:React Server Components のサポートcssVariables:テーマ設定に CSS 変数を使用しますprefix:Tailwind クラスのプレフィックス(任意)
Tailwind の設定
// tailwind.config.ts
import type { Config } from "tailwindcss"
const config = {
darkMode: ["class"],
content: [
'./pages/**/*.{ts,tsx}',
'./components/**/*.{ts,tsx}',
'./app/**/*.{ts,tsx}',
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config
export default config
CSS 変数(globals.css)
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
コンポーネント一覧
Button
import { Button } from "@/components/ui/button"
// Variants
<Button variant="default">Default</Button>
<Button variant="destructive">Destructive</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="link">Link</Button>
// Sizes
<Button size="default">Default</Button>
<Button size="sm">Small</Button>
<Button size="lg">Large</Button>
<Button size="icon"><Icon /></Button>
// States
<Button disabled>Disabled</Button>
<Button asChild>
<Link href="/about">As Link</Link>
</Button>
実装パターン(CVA):
import { cva, type VariantProps } from "class-variance-authority"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
Card
import {
Card,
CardHeader,
CardFooter,
CardTitle,
CardDescription,
CardContent,
} from "@/components/ui/card"
<Card>
<CardHeader>
<CardTitle>Card Title</CardTitle>
<CardDescription>Card description goes here</CardDescription>
</CardHeader>
<CardContent>
<p>Card content</p>
</CardContent>
<CardFooter>
<Button>Action</Button>
</CardFooter>
</Card>
Dialog(モーダル)
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
DialogFooter,
} from "@/components/ui/dialog"
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Are you sure?</DialogTitle>
<DialogDescription>
This action cannot be undone.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline">Cancel</Button>
<Button>Confirm</Button>
</DialogFooter>
</DialogContent>
</Dialog>
Form(react-hook-form + zod を使用)
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"
const formSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
email: z.string().email({
message: "Please enter a valid email address.",
}),
})
function ProfileForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values)
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="shadcn" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="[email protected]" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
)
}
Table
import {
Table,
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
<Table>
<TableCaption>A list of your recent invoices.</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Invoice</TableHead>
<TableHead>Status</TableHead>
<TableHead>Method</TableHead>
<TableHead className="text-right">Amount</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>INV001</TableCell>
<TableCell>Paid</TableCell>
<TableCell>Credit Card</TableCell>
<TableCell className="text-right">$250.00</TableCell>
</TableRow>
</TableBody>
</Table>
その他のコンポーネント
CLI から利用可能:
accordion- 折りたたみ可能なコンテンツセクションalert- コンテキストに応じたフィードバックメッセージalert-dialog- 操作を中断するモーダルダイアログavatar- ユーザープロフィール画像badge- ステータス表示calendar- 日付ピッカーcheckbox- 2 値入力command- コマンドパレット(⌘K メニュー)context-menu- 右クリックメニューdropdown-menu- ドロップダウンメニューhover-card- ホバー時のツールチップinput- テキスト入力label- フォームのラベルmenubar- アプリケーションのメニューバーnavigation-menu- サイトナビゲーションpopover- フローティングパネルprogress- 進捗インジケーターradio-group- ラジオボタングループscroll-area- カスタムスクロールバーselect- ドロップダウン選択separator- 視覚的な区切りsheet- サイドパネルskeleton- 読み込み時のプレースホルダーslider- 範囲入力switch- トグルスイッチtabs- タブナビゲーションtextarea- 複数行入力toast- 通知トーストtoggle- トグルボタンtooltip- ホバー時のツールチップ
テーマ設定
色のカスタマイズ
基本の配色を変更する:
# Regenerate components with new base color
npx shadcn-ui@latest init
# Choose new base: Slate, Gray, Zinc, Neutral, Stone
色を手動で上書きする(globals.css):
:root {
--primary: 210 100% 50%; /* HSL: Blue */
--primary-foreground: 0 0% 100%;
}
.dark {
--primary: 210 100% 60%; /* Lighter blue for dark mode */
}
カスタムバリアント
// Extend button variants
const buttonVariants = cva(
"...",
{
variants: {
variant: {
// ...existing variants
gradient: "bg-gradient-to-r from-purple-500 to-pink-500 text-white",
},
},
}
)
// Usage
<Button variant="gradient">Gradient Button</Button>
テーマの切り替え
// Using next-themes
import { ThemeProvider } from "next-themes"
// app/layout.tsx
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
{children}
</ThemeProvider>
</body>
</html>
)
}
// Theme toggle component
import { Moon, Sun } from "lucide-react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ThemeToggle() {
const { setTheme, theme } = useTheme()
return (
<Button
variant="ghost"
size="icon"
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
>
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
)
}
ダークモード
Next.js でのセットアップ
npm install next-themes
// app/providers.tsx
"use client"
import { ThemeProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
)
}
// app/layout.tsx
import { Providers } from "./providers"
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
ダークモードのユーティリティ
// Force dark mode for specific section
<div className="dark">
<Card>Always dark, regardless of theme</Card>
</div>
// Conditional styling
<div className="bg-white dark:bg-slate-950">
<p className="text-slate-900 dark:text-slate-50">
Adapts to theme
</p>
</div>
Next.js 統合
App Router のセットアップ
# Create Next.js app with TypeScript and Tailwind
npx create-next-app@latest my-app --typescript --tailwind --app
# Initialize shadcn/ui
cd my-app
npx shadcn-ui@latest init
# Add components
npx shadcn-ui@latest add button card form
サーバーコンポーネント
// app/page.tsx (Server Component by default)
import { Button } from "@/components/ui/button"
export default function HomePage() {
return (
<main>
<h1>Welcome</h1>
{/* Static components work in Server Components */}
<Button asChild>
<a href="/about">Learn More</a>
</Button>
</main>
)
}
クライアントコンポーネント
// app/interactive.tsx
"use client"
import { useState } from "react"
import { Button } from "@/components/ui/button"
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"
export function InteractiveSection() {
const [open, setOpen] = useState(false)
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogContent>
<p>Client-side interactivity</p>
</DialogContent>
</Dialog>
)
}
ルートハンドラー
// app/api/submit/route.ts
import { NextResponse } from "next/server"
import { z } from "zod"
const formSchema = z.object({
email: z.string().email(),
message: z.string().min(10),
})
export async function POST(request: Request) {
try {
const body = await request.json()
const validatedData = formSchema.parse(body)
// Process form data
return NextResponse.json({ success: true })
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json({ errors: error.errors }, { status: 400 })
}
return NextResponse.json({ error: "Internal error" }, { status: 500 })
}
}
アクセシビリティ
ARIA サポート
すべての shadcn/ui コンポーネントには、Radix UI による適切な ARIA 属性が含まれています。
// Dialog automatically includes:
// - role="dialog"
// - aria-describedby
// - aria-labelledby
// - Focus trap
// - Escape key handler
<Dialog>
<DialogContent>
{/* Automatically accessible */}
</DialogContent>
</Dialog>
// Button includes:
// - role="button"
// - tabindex="0"
// - Keyboard activation (Space/Enter)
<Button>Accessible by default</Button>
キーボード操作
組み込みのキーボードサポート:
Tab/Shift+Tab- インタラクティブな要素間を移動しますEnter/Space- ボタンを有効にしますEscape- ダイアログ、ドロップダウン、ポップオーバーを閉じますArrow keys- メニューや選択肢、ラジオグループ内を移動しますHome/End- リストの最初/最後の項目に移動します
例:コマンドパレット:
import {
Command,
CommandDialog,
CommandInput,
CommandList,
CommandEmpty,
CommandGroup,
CommandItem,
} from "@/components/ui/command"
// ⌘K to open
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput placeholder="Type a command..." />
<CommandList>
<CommandEmpty>No results found.</CommandEmpty>
<CommandGroup heading="Suggestions">
<CommandItem>Calendar</CommandItem>
<CommandItem>Search Emoji</CommandItem>
<CommandItem>Calculator</CommandItem>
</CommandGroup>
</CommandList>
</CommandDialog>
スクリーンリーダーのサポート
// Visually hidden but accessible to screen readers
<span className="sr-only">Close dialog</span>
// Skip navigation links
<a href="#main-content" className="sr-only focus:not-sr-only">
Skip to main content
</a>
// Descriptive labels
<FormLabel htmlFor="email">Email address</FormLabel>
<Input
id="email"
type="email"
aria-describedby="email-description"
aria-invalid={!!errors.email}
/>
<FormDescription id="email-description">
We'll never share your email.
</FormDescription>
フォーカス管理
// Focus trap in Dialog (automatic)
<Dialog>
<DialogContent>
{/* Focus stays within dialog until closed */}
</DialogContent>
</Dialog>
// Custom focus management
import { useRef, useEffect } from "react"
function CustomComponent() {
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
return <Input ref={inputRef} />
}
コンポジションパターン
複合コンポーネント
// Card composition
<Card>
<CardHeader>
<CardTitle>Title</CardTitle>
<CardDescription>Description</CardDescription>
</CardHeader>
<CardContent>Content</CardContent>
<CardFooter>Footer</CardFooter>
</Card>
// Form composition
<Form {...form}>
<FormField
control={form.control}
name="field"
render={({ field }) => (
<FormItem>
<FormLabel>Label</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormDescription>Help text</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</Form>
ポリモーフィックコンポーネント(asChild)
// Render Button as Link
import { Button } from "@/components/ui/button"
import Link from "next/link"
<Button asChild>
<Link href="/dashboard">Go to Dashboard</Link>
</Button>
// Render as custom component
<Button asChild>
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Animated Button
</motion.button>
</Button>
仕組み(Radix Slot):
import { Slot } from "@radix-ui/react-slot"
interface ButtonProps {
asChild?: boolean
}
const Button = ({ asChild, ...props }: ButtonProps) => {
const Comp = asChild ? Slot : "button"
return <Comp {...props} />
}
カスタムコンポジション
// Create custom card variant
export function PricingCard({
title,
price,
features,
highlighted
}: PricingCardProps) {
return (
<Card className={cn(highlighted && "border-primary")}>
<CardHeader>
<CardTitle>{title}</CardTitle>
<CardDescription className="text-3xl font-bold">
${price}/mo
</CardDescription>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{features.map((feature) => (
<li key={feature} className="flex items-center">
<Check className="mr-2 h-4 w-4 text-primary" />
{feature}
</li>
))}
</ul>
</CardContent>
<CardFooter>
<Button className="w-full" variant={highlighted ? "default" : "outline"}>
Get Started
</Button>
</CardFooter>
</Card>
)
}
CLI コマンド
初期化
# Interactive init
npx shadcn-ui@latest init
# Non-interactive with defaults
npx shadcn-ui@latest init -y
# Specify options
npx shadcn-ui@latest init --typescript --tailwind
コンポーネントの追加
# Single component
npx shadcn-ui@latest add button
# Multiple components
npx shadcn-ui@latest add button card dialog form
# All components (not recommended - adds everything)
npx shadcn-ui@latest add --all
# Specific version
npx shadcn-ui@latest add [email protected]
# Overwrite existing
npx shadcn-ui@latest add button --overwrite
# Different path
npx shadcn-ui@latest add button --path src/components/ui
コンポーネントの差分確認
# Check for component updates
npx shadcn-ui@latest diff
# Diff specific component
npx shadcn-ui@latest diff button
# Show what would change
npx shadcn-ui@latest diff --check
コンポーネントの更新
# Update all components
npx shadcn-ui@latest update
# Update specific components
npx shadcn-ui@latest update button card
# Preview changes before applying
npx shadcn-ui@latest update --dry-run
高度なパターン
カスタムフック
// useToast hook (built-in with toast component)
import { useToast } from "@/components/ui/use-toast"
function MyComponent() {
const { toast } = useToast()
return (
<Button
onClick={() => {
toast({
title: "Scheduled: Catch up",
description: "Friday, February 10, 2023 at 5:57 PM",
})
}}
>
Show Toast
</Button>
)
}
// Custom form hook
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
function useFormWithToast<T extends z.ZodType>(schema: T) {
const { toast } = useToast()
const form = useForm({
resolver: zodResolver(schema),
})
const handleSubmit = form.handleSubmit(async (data) => {
try {
// Submit logic
toast({ title: "Success!" })
} catch (error) {
toast({ title: "Error", variant: "destructive" })
}
})
return { form, handleSubmit }
}
レスポンシブデザイン
// Mobile-first responsive components
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<Card>Mobile: 1 col, Tablet: 2 col, Desktop: 3 col</Card>
</div>
// Responsive dialog (sheet on mobile, dialog on desktop)
import { useMediaQuery } from "@/hooks/use-media-query"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { Sheet, SheetContent } from "@/components/ui/sheet"
function ResponsiveModal({ children, ...props }) {
const isDesktop = useMediaQuery("(min-width: 768px)")
if (isDesktop) {
return (
<Dialog {...props}>
<DialogContent>{children}</DialogContent>
</Dialog>
)
}
return (
<Sheet {...props}>
<SheetContent>{children}</SheetContent>
</Sheet>
)
}
アニメーションのバリエーション
// Using Framer Motion with shadcn/ui
import { motion } from "framer-motion"
import { Card } from "@/components/ui/card"
const MotionCard = motion(Card)
<MotionCard
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
Animated Card
</MotionCard>
// Staggered list animation
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1
}
}
}
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 }
}
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((item) => (
<motion.li key={item.id} variants={item}>
<Card>{item.content}</Card>
</motion.li>
))}
</motion.ul>
ベストプラクティス
コードの構成:
- shadcn/ui コンポーネントは
components/ui/に配置します(アプリのコンポーネントと混在させないでください) - カスタムコンポジションは
components/(ui/ の外)に作成します - 共通ユーティリティには
lib/utils.tsを使用します
カスタマイズ:
- プロジェクト内でコンポーネントを直接変更します(コードの所有権はあなたにあります)
- テーマ全体に適用する変更には CSS 変数を使用します
- 新しいスタイルを追加するには、CVA でバリエーションを拡張します
components.jsonを手動で編集しないでください(CLI を使用します)
パフォーマンス:
- ツリーシェイキングは自動で行われます(使用するものだけがインポートされます)
- 不要なラッパー要素を避けるには
asChildを使用します - 重いコンポーネント(Calendar、Command)は遅延読み込みします
- 可能な場合はサーバーコンポーネントを優先します(Next.js)
アクセシビリティ:
- コンポーネントから ARIA 属性を削除しないでください
- カスタムコンポジションのキーボード操作をテストします
- ダイアログ/モーダルではフォーカス管理を維持します
- 適用できる場合は
asChildとセマンティック HTML を使用します
TypeScript:
- エクスポートされた型(ButtonProps、CardProps など)を活用します
- バリエーションの型安全性には VariantProps を使用します
- フォームのバリデーションには厳密な null チェックを追加します
トラブルシューティング
インポートエラー:
# Check path aliases in tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}
Tailwind クラスが適用されない場合:
// Ensure content paths include your components
// tailwind.config.ts
content: [
'./src/components/**/*.{ts,tsx}', // Add this
'./src/app/**/*.{ts,tsx}',
]
ダークモードが機能しない場合:
// Add suppressHydrationWarning to <html>
<html lang="en" suppressHydrationWarning>
フォームのバリデーションが実行されない場合:
// Ensure FormMessage is included in FormField
<FormField>
<FormItem>
<FormControl>...</FormControl>
<FormMessage /> {/* Required for errors */}
</FormItem>
</FormField>
リソース
- 公式ドキュメント:https://ui.shadcn.com
- コンポーネントのソース:https://github.com/shadcn-ui/ui
- Radix UI ドキュメント:https://www.radix-ui.com
- Tailwind CSS:https://tailwindcss.com
- CVA ドキュメント:https://cva.style/docs
- 例:https://ui.shadcn.com/examples
- コミュニティ:https://discord.gg/shadcn-ui