mcpField
S
shadcn-ui

shadcn-ui

github.com/bobmatnycデザイン・UI78421 回インストール15 週間前に更新
リポジトリ

shadcn-ui について

shadcn-uiは、ReactとTailwind CSSのプロジェクト向け再利用可能なコンポーネント集です。CLIでコードをプロジェクトに追加するため、開発者がコードを所有し、直接カスタマイズできます。

主な機能

  1. 01Radix UIとTailwind CSSを基盤に採用
  2. 02CLIでコンポーネントのソースを追加
  3. 03コードとスタイルをカスタマイズ可能
  4. 04フォーム、ダイアログ、テーブルを収録
  5. 05テーマ設定とダークモードに対応
  6. 06キーボード操作とARIAに対応

活用シーン

  1. 01Reactアプリのインターフェース構築
  2. 02プロジェクトに合わせたスタイル変更
  3. 03フォーム、ダイアログ、ナビゲーションの構成
  4. 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>

リソース

コメント