ブログ記事

shadcn/ui完全マスター2025 - コンポーネントライブラリの革命

shadcn/uiはコピー&ペーストで使えるReactコンポーネントライブラリの革命。Radix UIとTailwind CSSを基盤に、美しくアクセシブルなコンポーネントを提供。特徴的なコード配布アプローチ、CLIセットアップ、カスタマイズ、実践的なプロジェクト構築まで完全ガイド。

10分で読めます
R
Rina
Daily Hack 編集長
Web開発
shadcn/ui React Tailwind CSS Radix UI UIライブラリ
shadcn/ui完全マスター2025 - コンポーネントライブラリの革命のヒーロー画像

shadcn/ui は、2024-2025 年で最も注目を集めている Reactコンポーネントライブラリです。従来の npmパッケージの概念を覆し、コードを直接プロジェクトに取り込む革新的なアプローチで、美しくアクセシブルなコンポーネントを提供します。

この記事で学べること

  • shadcn/ui の革命的なコード配布アプローチの理解
  • CLI を使った効率的なセットアップとコンポーネント管理
  • Radix UI と Tailwind CSS の連携による強力な基盤
  • テーマカスタマイズとブランド適応の実践方法
  • 実用的な Web アプリケーション構築のベストプラクティス

shadcn/uiとは何か

革命的なコンポーネント配布プラットフォーム

shadcn/ui は、従来の UI ライブラリの概念を根本から覆す革新的なアプローチです:

  • コード所有権: npmパッケージではなく、コードを直接プロジェクトにコピー
  • コピー&ペースト: 必要なコンポーネントのみを選択的に導入
  • 完全カスタマイズ可能: ソースコードを直接編集可能
  • アクセシビリティファースト: Radix UI を基盤にした完全アクセシブルな設計

既存UIライブラリとの比較

主要UIライブラリの比較
項目 shadcn/ui Material-UI Chakra UI Ant Design
配布方式 コードコピー npmパッケージ npmパッケージ npmパッケージ
カスタマイズ性 非常に高い 中程度 高い 中程度
バンドルサイズ 必要最小 大きい 中程度 非常に大きい
アクセシビリティ 優秀 良い 優秀 普通
スタイル系 Tailwind CSS CSS-in-JS CSS-in-JS Less/CSS

プロジェクトセットアップ

節前提条件

shadcn/ui を使用するためには以下が必要です:

  • React 18+: モダン React機能をフル活用
  • TypeScript: 型安全性と開発体験の向上
  • Tailwind CSS: スタイリングシステムの基盤
  • Next.js 13+ (推奨): App Router での最適な統合

新規プロジェクトの作成

# Next.jsプロジェクトの作成
npx create-next-app@latest my-app --typescript --tailwind --eslint
cd my-app

# shadcn/ui CLIのインストール
npx shadcn@latest init
# Vite + Reactプロジェクトの作成
npm create vite@latest my-app -- --template react-ts
cd my-app
npm install

# Tailwind CSSのセットアップ
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

# shadcn/uiの初期化
npx shadcn@latest init
# Remixプロジェクトの作成
npx create-remix@latest my-app
cd my-app

# Tailwind CSSとshadcn/uiのセットアップ
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npx shadcn@latest init
# Astro + Reactプロジェクトの作成
npm create astro@latest my-app -- --template minimal
cd my-app
npx astro add react tailwind

# shadcn/uiのセットアップ
npx shadcn@latest init

CLI初期化の詳細設定

CLI 初期化時に以下の設定を行います:

npx shadcn@latest init

設定項目:

  • TypeScript: Yes (推奨)
  • スタイル: Default または New York
  • ベースカラー: Slate, Gray, Zinc, Neutral, Stone
  • CSS変数: Yes (推奨)
  • コンポーネントディレクトリ: src/components または components
  • utilsファイル: src/lib/utils または lib/utils

基本コンポーネントの使用

Buttonコンポーネントの導入

# Buttonコンポーネントのインストール
npx shadcn@latest add button

コンポーネントの基本使用方法:

import { Button } from '@/components/ui/button';

function MyComponent() {
  return (
    <div className="space-x-4">
      <Button>Default</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="destructive">Destructive</Button>
      <Button variant="outline">Outline</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="link">Link</Button>
    </div>
  );
}

Cardコンポーネントの活用

# Card関連コンポーネントのインストール
npx shadcn@latest add card
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';

function ProductCard() {
  return (
    <Card className="w-96">
      <CardHeader>
        <CardTitle>shadcn/uiマスターコース</CardTitle>
        <CardDescription>
          モダンなReact開発のための完全ガイド
        </CardDescription>
      </CardHeader>
      <CardContent>
        <p>コンポーネントライブラリの革命を体験し、美しいUIを効率的に構築する方法を学びましょう。</p>
      </CardContent>
      <CardFooter className="flex justify-between">
        <Button variant="outline">詳細を見る</Button>
        <Button>購入する</Button>
      </CardFooter>
    </Card>
  );
}

フォームコンポーネントの実装

高機能なフォームの構築

# フォーム関連コンポーネントのインストール
npx shadcn@latest add form input label button

React Hook Form と Zod を組み合わせた型安全なフォーム:

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { 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 contactSchema = z.object({
  name: z.string().min(2, '名前は2文字以上で入力してください'),
  email: z.string().email('有効なメールアドレスを入力してください'),
  message: z.string().min(10, 'メッセージは10文字以上で入力してください')
});

type ContactFormData = z.infer<typeof contactSchema>;

function ContactForm() {
  const form = useForm<ContactFormData>({
    resolver: zodResolver(contactSchema),
    defaultValues: {
      name: '',
      email: '',
      message: ''
    }
  });

  const onSubmit = (data: ContactFormData) => {
    console.log('フォームデータ:', data);
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>お名前</FormLabel>
              <FormControl>
                <Input placeholder="山田太郎" {...field} />
              </FormControl>
              <FormDescription>
                公開される表示名です。
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <FormField
          control={form.control}
          name="email"
          render={({ field }) => (
            <FormItem>
              <FormLabel>メールアドレス</FormLabel>
              <FormControl>
                <Input type="email" placeholder="example@company.com" {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <FormField
          control={form.control}
          name="message"
          render={({ field }) => (
            <FormItem>
              <FormLabel>メッセージ</FormLabel>
              <FormControl>
                <textarea
                  className="min-h-24 w-full rounded-md border border-input bg-background px-3 py-2"
                  placeholder="お問い合わせ内容をこちらに記載してください"
                  {...field}
                />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        
        <Button type="submit" className="w-full">
          送信する
        </Button>
      </form>
    </Form>
  );
}

テーマカスタマイズ

カラーシステムのカスタマイズ

shadcn/ui は、CSS 変数を使用した柔軟なテーマシステムを提供します:

/* globals.css */
@layer base {
  :root {
    /* ライトテーマ */
    --background: 0 0% 100%;
    --foreground: 222.2 84% 4.9%;
    --primary: 221.2 83.2% 53.3%;
    --primary-foreground: 210 40% 98%;
    --secondary: 210 40% 96%;
    --secondary-foreground: 222.2 84% 4.9%;
    --muted: 210 40% 96%;
    --muted-foreground: 215.4 16.3% 46.9%;
    --accent: 210 40% 96%;
    --accent-foreground: 222.2 84% 4.9%;
    --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: 221.2 83.2% 53.3%;
    --radius: 0.5rem;
  }
 
  .dark {
    /* ダークテーマ */
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --primary: 217.2 91.2% 59.8%;
    --primary-foreground: 222.2 84% 4.9%;
    --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: 224.3 76.3% 94.1%;
  }
}

カスタムテーマの作成

:root {
  --primary: 221.2 83.2% 53.3%; /* ブルー */
  --secondary: 210 40% 96%;      /* グレー */
  --accent: 210 40% 96%;         /* グレー */
}
:root {
  --primary: 262 100% 47%;       /* パープル */
  --secondary: 230 100% 60%;     /* ブルー */
  --accent: 340 100% 60%;        /* ピンク */
}
デフォルトテーマ
:root {
  --primary: 221.2 83.2% 53.3%; /* ブルー */
  --secondary: 210 40% 96%;      /* グレー */
  --accent: 210 40% 96%;         /* グレー */
}
カスタムテーマ
:root {
  --primary: 262 100% 47%;       /* パープル */
  --secondary: 230 100% 60%;     /* ブルー */
  --accent: 340 100% 60%;        /* ピンク */
}

ダークモードの実装

// app/layout.tsx
import { ThemeProvider } from '@/components/theme-provider';

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  );
}
// components/theme-toggle.tsx
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';

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

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

複雑なコンポーネントの構築

Data Tableの実装

# データテーブル関連コンポーネント
npx shadcn@latest add table checkbox dropdown-menu

TanStack Table との統合例:

import {
  ColumnDef,
  flexRender,
  getCoreRowModel,
  getSortedRowModel,
  useReactTable,
} from '@tanstack/react-table';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
import { Button } from '@/components/ui/button';
import { ArrowUpDown } from 'lucide-react';

type User = {
  id: string;
  name: string;
  email: string;
  role: string;
  status: 'active' | 'inactive';
};

const columns: ColumnDef<User>[] = [
  {
    id: 'select',
    header: ({ table }) => (
      <Checkbox
        checked={table.getIsAllPageRowsSelected()}
        onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
      />
    ),
    cell: ({ row }) => (
      <Checkbox
        checked={row.getIsSelected()}
        onCheckedChange={(value) => row.toggleSelected(!!value)}
      />
    ),
  },
  {
    accessorKey: 'name',
    header: ({ column }) => (
      <Button
        variant="ghost"
        onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
      >
        名前
        <ArrowUpDown className="ml-2 h-4 w-4" />
      </Button>
    ),
  },
  {
    accessorKey: 'email',
    header: 'メール',
  },
  {
    accessorKey: 'role',
    header: '役職',
  },
  {
    accessorKey: 'status',
    header: 'ステータス',
    cell: ({ row }) => {
      const status = row.getValue('status') as string;
      return (
        <span
          className={`px-2 py-1 rounded-full text-xs ${
            status === 'active'
              ? 'bg-green-100 text-green-800'
              : 'bg-red-100 text-red-800'
          }`}
        >
          {status === 'active' ? 'アクティブ' : '非アクティブ'}
        </span>
      );
    },
  },
];

function UserTable({ data }: { data: User[] }) {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
    getSortedRowModel: getSortedRowModel(),
  });

  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?.length ? (
            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>
            ))
          ) : (
            <TableRow>
              <TableCell colSpan={columns.length} className="h-24 text-center">
                データがありません
              </TableCell>
            </TableRow>
          )}
        </TableBody>
      </Table>
    </div>
  );
}

パフォーマンス最適化

バンドルサイズの最適化

shadcn/ui のコードコピーアプローチによるメリット:

必要なコンポーネントのみ 100 %
完了
使用しないコンポーネントもバンドルに含まれる 40 %
// 適切な Tree Shaking 設定
// next.config.js
module.exports = {
  experimental: {
    optimizePackageImports: ['@radix-ui/react-icons'],
  },
};

コンポーネントの遅延読み込み

// モーダルコンポーネントの遅延読み込み
import { lazy, Suspense } from 'react';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';

const HeavyComponent = lazy(() => import('@/components/heavy-component'));

function App() {
  const [showHeavy, setShowHeavy] = useState(false);

  return (
    <div>
      <Button onClick={() => setShowHeavy(true)}>
        重いコンポーネントを読み込み
      </Button>
      
      {showHeavy && (
        <Suspense fallback={<Skeleton className="w-full h-64" />}>
          <HeavyComponent />
        </Suspense>
      )}
    </div>
  );
}

実用的なプロジェクト構築

ダッシュボードアプリケーション

// app/dashboard/layout.tsx
import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/app-sidebar';

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <SidebarProvider>
      <AppSidebar />
      <SidebarInset>
        <header className="flex h-16 shrink-0 items-center gap-2 border-b px-4">
          <h1 className="text-lg font-semibold">ダッシュボード</h1>
        </header>
        <main className="flex-1 p-4">{children}</main>
      </SidebarInset>
    </SidebarProvider>
  );
}

ランディングページの構築

// components/hero-section.tsx
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';

function HeroSection() {
  return (
    <section className="container flex flex-col items-center gap-4 pb-8 pt-6 md:py-10">
      <div className="flex max-w-[980px] flex-col items-start gap-2">
        <Badge variant="outline">新登場</Badge>
        <h1 className="text-3xl font-bold leading-tight tracking-tighter md:text-5xl lg:leading-[1.1]">
          コンポーネントライブラリの
          <br className="hidden sm:inline" />
          新しいスタンダード
        </h1>
        <p className="max-w-[750px] text-lg text-muted-foreground sm:text-xl">
          美しくアクセシブルなコンポーネントで、
          あなたのアプリケーションを構築しましょう。
        </p>
      </div>
      <div className="flex gap-4">
        <Button size="lg">始める</Button>
        <Button variant="outline" size="lg">
          GitHubで確認
        </Button>
      </div>
    </section>
  );
}

アクセシビリティの実現

shadcn/ui は Radix UI を基盤としており、標準で高いアクセシビリティを提供します:

キーボードナビゲーション

// キーボード操作をサポートするメニュー
import {
  NavigationMenu,
  NavigationMenuContent,
  NavigationMenuItem,
  NavigationMenuLink,
  NavigationMenuList,
  NavigationMenuTrigger,
} from '@/components/ui/navigation-menu';

function AccessibleNavigation() {
  return (
    <NavigationMenu>
      <NavigationMenuList>
        <NavigationMenuItem>
          <NavigationMenuTrigger>サービス</NavigationMenuTrigger>
          <NavigationMenuContent>
            <ul className="grid gap-3 p-4 md:w-[400px] lg:w-[500px]">
              <li className="row-span-3">
                <NavigationMenuLink asChild>
                  <a
                    className="flex h-full w-full select-none flex-col justify-end rounded-md bg-gradient-to-b from-muted/50 to-muted p-6 no-underline outline-none focus:shadow-md"
                    href="/"
                  >
                    <div className="mb-2 mt-4 text-lg font-medium">
                      shadcn/ui
                    </div>
                    <p className="text-sm leading-tight text-muted-foreground">
                      美しくアクセシブルなコンポーネント
                    </p>
                  </a>
                </NavigationMenuLink>
              </li>
            </ul>
          </NavigationMenuContent>
        </NavigationMenuItem>
      </NavigationMenuList>
    </NavigationMenu>
  );
}

スクリーンリーダー対応

// スクリーンリーダーへの適切な情報提供
import { Button } from '@/components/ui/button';
import { Plus } from 'lucide-react';

function AccessibleButton() {
  return (
    <Button
      aria-label="新しい項目を追加"
      aria-describedby="add-item-description"
    >
      <Plus className="mr-2 h-4 w-4" aria-hidden="true" />
      追加
    </Button>
  );
}

ベストプラクティス

コンポーネントの組み合わせ

効率的なコンポーネント管理

  • 必要なコンポーネントのみをインストール
  • カスタマイズしたコンポーネントは別ディレクトリで管理
  • コンポーネントの再利用性を意識した設計
  • TypeScriptでの props の型安全性を保つ

プロジェクト構成の推奨例

project/
├── src/
│   ├── components/
│   │   ├── ui/              # shadcn/uiコンポーネント
│   │   │   ├── button.tsx
│   │   │   ├── input.tsx
│   │   │   └── form.tsx
│   │   ├── custom/          # カスタムコンポーネント
│   │   │   ├── user-profile.tsx
│   │   │   └── dashboard-card.tsx
│   │   └── layout/          # レイアウトコンポーネント
│   │       ├── header.tsx
│   │       └── sidebar.tsx
│   ├── lib/
│   │   ├── utils.ts          # ユーティリティ関数
│   │   └── constants.ts      # 定数定義
│   └── styles/
│       └── globals.css       # グローバルCSS
└── components.json           # shadcn/ui設定

まとめ

shadcn/ui は、Reactコンポーネントライブラリの概念を根本から変える革命的なアプローチです:

  • コード所有権: コンポーネントを直接プロジェクトにコピーし、完全なカスタマイズを実現
  • アクセシビリティファースト: Radix UI を基盤とした高品質なアクセシビリティ
  • 開発体験の向上: CLI ツールと TypeScriptでの効率的な開発
  • パフォーマンス: 必要なコンポーネントのみを含む最適化されたバンドル
  • コミュニティ驱動: オープンソースで活発なコミュニティ

従来の UI ライブラリの制約を脱却し、真に柔軟で持続可能な UI 開発を実現する shadcn/ui を、次のプロジェクトでぜひお試しください。

Rinaのプロフィール画像

Rina

Daily Hack 編集長

フルスタックエンジニアとして10年以上の経験を持つ。 大手IT企業やスタートアップでの開発経験を活かし、 実践的で即効性のある技術情報を日々発信中。 特にWeb開発、クラウド技術、AI活用に精通。

この記事は役に立ちましたか?

あなたのフィードバックが記事の改善に役立ちます

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。