mcpField
S
shadcn-ui

shadcn-ui

github.com/oimiragieoデザイン・UI4438 回インストール17 週間前に更新
リポジトリ

shadcn-ui について

ReactやNext.jsの開発者向けに、shadcn/uiのコンポーネント、スタイル、テーマ、アクセシビリティの使い方を案内するスキルです。コンポーネントはパッケージとして導入せず、CLIでプロジェクトにコピーして所有・カスタマイズします。

主な機能

  1. 01CLIでコンポーネントをプロジェクトにコピー
  2. 02Tailwind CSSの変数によるテーマ設定
  3. 03ダークモード設定の案内
  4. 04フォーム、テーブル、ダイアログの解説
  5. 05Radixベースのアクセシビリティパターン

活用シーン

  1. 01Reactプロジェクトへのshadcn/ui追加
  2. 02ダークモードとテーマトークンの設定
  3. 03アクセシブルなフォームやダイアログの構築
  4. 04コンポーネントと独自UIの選択

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/oimiragieo/agent-studio --skill shadcn-ui

インストール後、Claude Code で/shadcn-uiを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

commands/
shadcn-ui.md
hooks/
post-execute.cjs
pre-execute.cjs
references/
research-requirements.md
rules/
shadcn-ui.md
schemas/
input.schema.json
output.schema.json
scripts/
main.cjs
templates/
implementation-template.md

詳細

shadcn/ui エキスパート

shadcn/ui を使った UI 構築の包括的なガイドです。shadcn/ui は Radix UI のプリミティブと Tailwind CSS を基盤とする、コピーして使うコンポーネントライブラリです。コンポーネントは依存関係としてインストールされるのではなく、プロジェクトにコピーされるため、完全な所有権とカスタマイズ性が得られます。

適用する場面

次のような場合にこのスキルを使用します。

  • React または Next.js のプロジェクトに shadcn/ui コンポーネントを追加する
  • コンポーネントのスタイル、バリアント、動作をカスタマイズする
  • CSS 変数を使った Tailwind CSS v4 のテーマを設定する
  • shadcn/ui でダークモードを実装する
  • アクセシブルなフォーム、ダイアログ、データテーブルを構築する
  • shadcn/ui コンポーネントと独自実装を選択する

基本概念

shadcn/ui はコンポーネントライブラリではありません

shadcn/ui は、プロジェクトにコピーして使う再利用可能なコンポーネントのコレクションです。従来のライブラリとは次の点が異なります。

  • npm パッケージへの依存なし -- コンポーネントはコードベース内に配置されます
  • 完全な所有権 -- あらゆるコンポーネントを自由に変更できます
  • Radix UI のプリミティブ -- 内部では、アクセシブルでスタイルを持たないヘッドレスコンポーネントを使用します
  • Tailwind CSS -- すべてのスタイリングはユーティリティクラスと CSS 変数を使用します
  • CLI 主導 -- npx shadcn@latest add button を実行すると、コンポーネントのコードがコピーされます
アーキテクチャ
Your Project
  components/
    ui/              <- shadcn/ui components live here
      button.tsx
      dialog.tsx
      input.tsx
      ...
  lib/
    utils.ts         <- cn() utility (clsx + tailwind-merge)

セットアップ

Next.js App Router(推奨)
# Initialize shadcn/ui in existing Next.js project
npx shadcn@latest init

# This creates:
# - components.json (configuration)
# - lib/utils.ts (cn utility)
# - Tailwind CSS variable theme in globals.css

components.json の設定:

{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": true,
  "tsx": true,
  "tailwind": {
    "config": "tailwind.config.ts",
    "css": "app/globals.css",
    "baseColor": "zinc",
    "cssVariables": true
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils",
    "ui": "@/components/ui",
    "lib": "@/lib",
    "hooks": "@/hooks"
  }
}
Vite + React
# Initialize
npx shadcn@latest init

# Vite requires path aliases in vite.config.ts:
import path from 'path';

export default defineConfig({
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
});
Next.js Pages Router

App Router の場合と同じですが、Pages Router は React Server Components をサポートしていないため、components.json で rsc: false を設定します。

コンポーネントの追加

# Add a single component
npx shadcn@latest add button

# Add multiple components
npx shadcn@latest add button card input label

# Add all components
npx shadcn@latest add --all

# View available components
npx shadcn@latest add --list
cn() ユーティリティ

shadcn/ui のすべてのコンポーネントは、条件付きのクラスを結合するために cn() を使用します。

// lib/utils.ts
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}

cn() は clsx(条件付きクラス)と tailwind-merge(Tailwind の競合を解決)を組み合わせます。

<Button
  className={cn(
    'bg-primary text-white',
    isDisabled && 'opacity-50 cursor-not-allowed',
    size === 'lg' && 'px-8 py-4 text-lg'
  )}
>
  Submit
</Button>

CSS 変数を使ったテーマ設定

Tailwind CSS v4 のテーマ設定

shadcn/ui はテーマ設定に CSS カスタムプロパティを使用するため、実行時にテーマを切り替えられます。

/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  :root {
    --background: 0 0% 100%;
    --foreground: 240 10% 3.9%;
    --card: 0 0% 100%;
    --card-foreground: 240 10% 3.9%;
    --popover: 0 0% 100%;
    --popover-foreground: 240 10% 3.9%;
    --primary: 240 5.9% 10%;
    --primary-foreground: 0 0% 98%;
    --secondary: 240 4.8% 95.9%;
    --secondary-foreground: 240 5.9% 10%;
    --muted: 240 4.8% 95.9%;
    --muted-foreground: 240 3.8% 46.1%;
    --accent: 240 4.8% 95.9%;
    --accent-foreground: 240 5.9% 10%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 0 0% 98%;
    --border: 240 5.9% 90%;
    --input: 240 5.9% 90%;
    --ring: 240 5.9% 10%;
    --radius: 0.5rem;
  }

  .dark {
    --background: 240 10% 3.9%;
    --foreground: 0 0% 98%;
    --card: 240 10% 3.9%;
    --card-foreground: 0 0% 98%;
    --popover: 240 10% 3.9%;
    --popover-foreground: 0 0% 98%;
    --primary: 0 0% 98%;
    --primary-foreground: 240 5.9% 10%;
    --secondary: 240 3.7% 15.9%;
    --secondary-foreground: 0 0% 98%;
    --muted: 240 3.7% 15.9%;
    --muted-foreground: 240 5% 64.9%;
    --accent: 240 3.7% 15.9%;
    --accent-foreground: 0 0% 98%;
    --destructive: 0 62.8% 30.6%;
    --destructive-foreground: 0 0% 98%;
    --border: 240 3.7% 15.9%;
    --input: 240 3.7% 15.9%;
    --ring: 240 4.9% 83.9%;
  }
}
ダークモードの実装

Next.js でダークモードを使用する場合は、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>
  );
}

切り替えコンポーネント:

'use client';

import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
import { Moon, Sun } from 'lucide-react';

export function ThemeToggle() {
  const { setTheme, theme } = useTheme();

  return (
    <Button
      variant="ghost"
      size="icon"
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
    >
      <Sun className="h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
      <Moon className="absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
      <span className="sr-only">Toggle theme</span>
    </Button>
  );
}

よく使われるコンポーネントのパターン

React Hook Form + Zod を使ったフォーム
'use client';

import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import * as z from 'zod';
import { Button } from '@/components/ui/button';
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';

const formSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

export function LoginForm() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { email: '', password: '' },
  });

  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values);
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Email</FormLabel>
              <FormControl>
                <Input placeholder="[email protected]" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <FormField
          control={form.control}
          name="password"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Password</FormLabel>
              <FormControl>
                <Input type="password" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Sign In</Button>
      </form>
    </Form>
  );
}
TanStack Table を使ったデータテーブル
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from '@tanstack/react-table';

interface DataTableProps<TData, TValue> {
  columns: ColumnDef<TData, TValue>[];
  data: TData[];
}

export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });

  return (
    <div className="rounded-md border">
      <Table>
        <TableHeader>
          {table.getHeaderGroups().map(headerGroup => (
            <TableRow key={headerGroup.id}>
              {headerGroup.headers.map(header => (
                <TableHead key={header.id}>
                  {flexRender(header.column.columnDef.header, header.getContext())}
                </TableHead>
              ))}
            </TableRow>
          ))}
        </TableHeader>
        <TableBody>
          {table.getRowModel().rows.map(row => (
            <TableRow key={row.id}>
              {row.getVisibleCells().map(cell => (
                <TableCell key={cell.id}>
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </TableCell>
              ))}
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </div>
  );
}
レスポンシブなダイアログ/ドロワーのパターン

デスクトップでは Dialog、モバイルでは Drawer を使用します。

'use client';

import { useMediaQuery } from '@/hooks/use-media-query';
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer';
import { Button } from '@/components/ui/button';

export function ResponsiveModal({ children }: { children: React.ReactNode }) {
  const isDesktop = useMediaQuery('(min-width: 768px)');

  if (isDesktop) {
    return (
      <Dialog>
        <DialogTrigger asChild>
          <Button>Open</Button>
        </DialogTrigger>
        <DialogContent>{children}</DialogContent>
      </Dialog>
    );
  }

  return (
    <Drawer>
      <DrawerTrigger asChild>
        <Button>Open</Button>
      </DrawerTrigger>
      <DrawerContent>{children}</DrawerContent>
    </Drawer>
  );
}

アクセシビリティのパターン

shadcn/ui のコンポーネントは Radix UI を基盤としており、次の機能を提供します。

  • 完全なキーボード操作(Tab、矢印キー、Enter、Escape)
  • ARIA 属性(ロール、状態、プロパティ)
  • フォーカス管理(トラップ、復元)
  • スクリーンリーダー向けの通知
コンポーネントごとの主なアクセシビリティ機能
コンポーネントキーボードARIAフォーカストラップ
ButtonEnter/Space で有効化role="button"いいえ
DialogEscape で閉じるrole="dialog", aria-modalはい
Dropdown Menu矢印キーで移動role="menu", role="menuitem"はい
Select矢印キー、入力による検索role="listbox", role="option"はい
Tabs矢印キーでタブ間を移動role="tablist", role="tab"いいえ
Toast自動通知role="status", aria-liveいいえ
Tooltipフォーカス/ホバーで表示role="tooltip"いいえ
独自のアクセシビリティ強化
// Always provide labels for interactive elements
<Button aria-label="Close dialog">
  <X className="h-4 w-4" />
</Button>

// Use sr-only for visual-only content
<span className="sr-only">Loading...</span>

// Announce dynamic content
<div aria-live="polite" aria-atomic="true">
  {statusMessage}
</div>

避けるべきパターン

  • shadcn/ui を npm パッケージとしてインストールしないでください。CLI を使ってコンポーネントをコピーしてください。
  • Radix のプリミティブを直接変更しないでください。shadcn のラッパーコンポーネントを拡張してください。
  • 色をハードコードしないでください。必ず CSS 変数のテーマトークンを使ってください。
  • cn() ユーティリティを省略しないでください。Tailwind クラスの競合を防ぎます。
  • next-themes を使用する場合は、<html> に suppressHydrationWarning を設定し忘れないでください。
  • インタラクティブ要素を入れ子にしないでください(ボタン内のボタン、ボタン内のリンクなど)。

厳守事項

  1. shadcn/ui をパッケージ依存関係としてインストールしては絶対にいけません。コンポーネントは完全に所有できるよう、プロジェクトにコピーする必要があります
  2. 条件付きのクラス名には必ず cn() ユーティリティを使用し、Tailwind のクラス競合を防いでください
  3. 色をハードコードしては絶対にいけません。テーマの一貫性を保つため、必ず CSS 変数のテーマトークンを使用してください
  4. Radix UI プリミティブは必ず shadcn/ui の抽象化を介して使用し、直接使用してはいけません
  5. インタラクティブ要素を入れ子にしては絶対にいけません(ボタン内にボタン、ボタン内にリンクなど)。アクセシビリティ基準に違反します

アンチパターン

アンチパターン問題が起きる理由正しい方法
パッケージとしてインストールするコンポーネントのソースが固定され、カスタマイズできませんnpx shadcn@latest add を使ってコンポーネントをプロジェクトにコピーします
色の値をハードコードするテーマの切り替えが機能せず、ダークモードも動作しませんCSS 変数のトークン(bg-background、text-foreground など)を使います
cn() ユーティリティを省略するTailwind のクラス競合により、スタイルが予測不能になります@/lib/utils の cn() でクラスを必ずマージします
Radix UI プリミティブを直接使うshadcn のスタイルとアクセシビリティの設定が不足しますRadix プリミティブを適切なクラスでラップした shadcn コンポーネントを使います
suppressHydrationWarning を設定しないnext-themes のダークモードでハイドレーションの不一致エラーが発生しますnext-themes を使う場合は <html> に suppressHydrationWarning を追加します

メモリープロトコル(必須)

開始前: .claude/context/memory/learnings.md を読みます

完了後:

  • 新しいパターン -> .claude/context/memory/learnings.md
  • 発見した問題 -> .claude/context/memory/issues.md
  • 決定事項 -> .claude/context/memory/decisions.md

中断されることを前提としてください。メモリーに記録されていなければ、実行されていないものとします。

参考資料

コメント