T-CREATOR

shadcn/ui × Next.js:モダンな UI を爆速構築する方法

shadcn/ui × Next.js:モダンな UI を爆速構築する方法

Web 開発の現場で、美しく機能的な UI を素早く構築したいと思われたことはありませんでしょうか。従来の UI ライブラリでは、カスタマイズに時間がかかったり、デザインの制約に悩まされることが多いですね。

そんな課題を解決してくれるのが、shadcn/ui と Next.js の組み合わせです。この二つの技術を使うことで、モダンで洗練された UI を驚くほど短時間で構築できるようになります。特に初心者の方でも、プロフェッショナルな見た目の Web アプリケーションを作ることが可能です。

今回の記事では、shadcn/ui と Next.js を使って、実際に手を動かしながらモダンな UI を構築する方法をステップバイステップで解説していきます。

shadcn/ui とは何か

従来の UI ライブラリとは一線を画すアプローチ

従来の UI ライブラリの多くは、npm パッケージとしてインストールして使用する形式でした。これらのライブラリには以下のような特徴がございます:

typescript// 従来のUIライブラリの例(Material-UI)
import { Button, TextField, Card } from '@mui/material';

function MyComponent() {
  return (
    <Card>
      <TextField label='ユーザー名' />
      <Button variant='contained'>送信</Button>
    </Card>
  );
}

しかし、shadcn/ui はコピー&ペースト方式という革新的なアプローチを採用しています。これは、必要なコンポーネントのソースコードを直接プロジェクトにコピーして使用する方法です。

以下の図で、従来の UI ライブラリと shadcn/ui の違いを比較してみましょう:

mermaidflowchart TD
    A[従来のUIライブラリ] --> B[npm install]
    B --> C[ライブラリ依存]
    C --> D[カスタマイズ制限]

    E[shadcn/ui] --> F[コピー&ペースト]
    F --> G[完全な所有権]
    G --> H[自由なカスタマイズ]

コンポーネント駆動開発の利点

shadcn/ui は、現代的なコンポーネント駆動開発の考え方を基盤としています。各 UI パーツを独立したコンポーネントとして管理することで、以下のメリットが得られます。

再利用性が高まり、一度作成したコンポーネントを様々な場所で使い回すことができます。また、テストしやすく、メンテナンスも容易になりますね。

#従来の方法shadcn/ui
1HTML 直書きコンポーネント化
2スタイル散乱統一されたデザイン
3重複コード再利用可能

Next.js との相性が抜群な理由

shadcn/ui が Next.js と相性が良い理由は、両者が同じ技術スタックを基盤としているためです:

  • TypeScript 完全対応: 型安全性を保ちながら開発できます
  • Tailwind CSS 活用: ユーティリティファーストなスタイリング
  • React Server Components 対応: 最新の Next.js 機能と連携
typescript// Next.js App Routerでの使用例
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';

export default function HomePage() {
  return (
    <div className='container mx-auto p-4'>
      <Card className='p-6'>
        <h1 className='text-2xl font-bold mb-4'>
          ようこそ
        </h1>
        <Button>始める</Button>
      </Card>
    </div>
  );
}

導入前の課題

UI コンポーネントの自作コスト

Web アプリケーションを開発する際、UI コンポーネントを一から作成するには膨大な時間とコストがかかります。例えば、シンプルなボタン一つ作るだけでも、以下のような作業が必要になります。

css/* ボタンの基本スタイル */
.button {
  padding: 8px 16px;
  border-radius: 4px;
  border: none;
  cursor: pointer;
  transition: all 0.2s;
}

/* ホバー状態 */
.button:hover {
  opacity: 0.8;
}

/* フォーカス状態 */
.button:focus {
  outline: 2px solid blue;
}

さらに、レスポンシブデザイン、アクセシビリティ対応、ダークモード対応など、考慮すべき要素は無数にあります。これらすべてを一人の開発者が完璧に実装するのは現実的ではありませんね。

デザインシステムの統一性

複数の開発者がプロジェクトに関わる場合、デザインの統一性を保つことは大きな課題となります。

以下の図は、統一性がない場合に起こりがちな問題を表しています:

mermaidflowchart LR
    A[開発者A] -->|異なるスタイル| D[ユーザー体験の悪化]
    B[開発者B] -->|異なるスタイル| D
    C[開発者C] -->|異なるスタイル| D

    D --> E[ブランドイメージ低下]
    D --> F[メンテナンス困難]

実際のプロジェクトでは、以下のような問題が発生することがあります:

  • ボタンのサイズや色が画面ごとに異なる
  • フォントサイズや余白の基準がバラバラ
  • インタラクションの挙動が統一されていない

開発スピードの問題

現代の Web 開発では、迅速なプロトタイピングと素早いリリースが求められます。しかし、UI コンポーネントの開発に時間をかけすぎると、以下のような問題が生じてしまいます。

#問題影響
1開発期間の長期化プロジェクトの遅延
2コストの増大予算オーバー
3市場投入の遅れ競合他社に遅れを取る

特にスタートアップや小規模なチームでは、限られたリソースの中で最大の成果を出す必要があります。そのため、UI 開発にかかる時間を最小限に抑えることが重要になります。

shadcn/ui × Next.js による解決策

Copy & Paste による迅速な導入

shadcn/ui の最大の特徴は、コンポーネントを「コピー&ペースト」で導入できることです。これは従来の npm パッケージベースのライブラリとは根本的に異なるアプローチですね。

以下の図で、従来の方法と shadcn/ui の導入フローを比較してみましょう:

mermaidsequenceDiagram
    participant Dev as 開発者
    participant CLI as shadcn CLI
    participant Project as プロジェクト

    Dev->>CLI: npx shadcn-ui@latest add button
    CLI->>Project: コンポーネントファイル生成
    Project->>Dev: 即座に利用可能

    Note over Dev,Project: 数秒で完了!

実際のコマンド実行例:

bash# Buttonコンポーネントを追加
npx shadcn-ui@latest add button

# 複数コンポーネントを一括追加
npx shadcn-ui@latest add button card input

コマンドを実行すると、components​/​uiディレクトリに必要なファイルが生成されます。

typescript// 生成されるButtonコンポーネント(例)
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import {
  cva,
  type VariantProps,
} from 'class-variance-authority';
import { cn } from '@/lib/utils';

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors',
  {
    variants: {
      variant: {
        default:
          'bg-primary text-primary-foreground hover:bg-primary/90',
        destructive:
          'bg-destructive text-destructive-foreground hover:bg-destructive/90',
      },
      size: {
        default: 'h-10 px-4 py-2',
        sm: 'h-9 rounded-md px-3',
        lg: 'h-11 rounded-md px-8',
      },
    },
    defaultVariants: {
      variant: 'default',
      size: 'default',
    },
  }
);

TypeScript 完全対応

shadcn/ui のすべてのコンポーネントは、TypeScript で記述されており、完璧な型安全性を提供します。これにより、開発時のエラーを大幅に減少させることができます。

typescript// 型安全なコンポーネント使用例
import { Button } from "@/components/ui/button"

// ✅ 正しい使い方
<Button variant="default" size="lg">
  クリック
</Button>

// ❌ TypeScriptがエラーを検出
<Button variant="invalid" size="wrong">
  エラーが出る
</Button>

プロパティの候補も自動補完されるため、開発効率が大幅に向上します。

カスタマイズ性の高さ

shadcn/ui の真価は、その圧倒的なカスタマイズ性にあります。コンポーネントのソースコードが直接プロジェクトに含まれるため、必要に応じて自由に修正できます。

以下は、Button コンポーネントに独自のバリアントを追加する例です:

typescript// buttonVariantsにカスタムバリアント追加
const buttonVariants = cva(
  // ... 既存のベーススタイル
  {
    variants: {
      variant: {
        default:
          'bg-primary text-primary-foreground hover:bg-primary/90',
        // 🎨 カスタムバリアント追加
        gradient:
          'bg-gradient-to-r from-purple-500 to-pink-500 text-white hover:from-purple-600 hover:to-pink-600',
        outline:
          'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
      },
      // ... その他の設定
    },
  }
);

カスタマイズした後の使用例:

typescript// カスタムバリアントの使用
<Button variant='gradient'>グラデーションボタン</Button>

このように、既存のコンポーネントベースを維持しながら、プロジェクト固有の要件に合わせて柔軟にカスタマイズできるのが、shadcn/ui の大きな魅力です。

環境構築から初回コンポーネント追加まで

Next.js プロジェクトの準備

まず、Next.js の新しいプロジェクトを作成します。TypeScript と Tailwind CSS を含む構成で初期化しましょう。

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

作成されたプロジェクト構造は以下のようになります:

luamy-shadcn-app/
├── app/
│   ├── globals.css
│   ├── layout.tsx
│   └── page.tsx
├── components/
├── public/
├── next.config.js
├── package.json
├── tailwind.config.ts
└── tsconfig.json

プロジェクトディレクトリに移動して、開発サーバーを起動できることを確認しましょう。

bash# プロジェクトディレクトリに移動
cd my-shadcn-app

# 依存関係をインストール
yarn install

# 開発サーバー起動
yarn dev

shadcn/ui の初期設定

Next.js プロジェクトの準備ができたら、shadcn/ui の初期設定を行います。この設定により、コンポーネントの配置先やスタイル設定が決定されます。

bash# shadcn/ui初期化
npx shadcn-ui@latest init

初期化コマンドを実行すると、以下のような質問が表示されます:

#質問推奨回答説明
1TypeScript 使用?Yes型安全性のため
2スタイルは?Default標準的な設定
3基本色は?Slate汎用性が高い
4CSS 変数使用?Yesカスタマイズ性向上

初期化が完了すると、以下のファイルが自動生成されます:

typescript// components.json - shadcn/ui設定ファイル
{
  "$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
  },
  "aliases": {
    "components": "@/components",
    "utils": "@/lib/utils"
  }
}

また、ユーティリティ関数も自動生成されます:

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

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

初回コンポーネント(Button)の追加

環境構築が完了したら、最初のコンポーネントとして Button を追加してみましょう。

bash# Buttonコンポーネントを追加
npx shadcn-ui@latest add button

コマンド実行後、以下の場所に Button コンポーネントが生成されます:

csscomponents/
└── ui/
    └── button.tsx

生成された Button コンポーネントを使用してみましょう:

typescript// app/page.tsx
import { Button } from '@/components/ui/button';

export default function Home() {
  return (
    <main className='flex min-h-screen flex-col items-center justify-center p-24'>
      <div className='space-y-4'>
        <h1 className='text-4xl font-bold text-center'>
          shadcn/ui × Next.js
        </h1>

        <div className='flex gap-4'>
          <Button variant='default'>デフォルト</Button>
          <Button variant='secondary'>セカンダリー</Button>
          <Button variant='outline'>アウトライン</Button>
          <Button variant='destructive'>削除</Button>
        </div>
      </div>
    </main>
  );
}

このコードを保存すると、ブラウザで以下のような美しいボタンが表示されます。各ボタンは異なるスタイルバリアントを持ち、ホバー効果やフォーカス状態も自動的に適用されています。

実践例:ダッシュボード画面を 30 分で構築

必要コンポーネントの選定

ダッシュボード画面を構築するため、以下のコンポーネントが必要になります。各コンポーネントの役割と使用場面を整理してみましょう。

mermaidflowchart TD
    A[ダッシュボード画面] --> B[Card]
    A --> C[Button]
    A --> D[Badge]
    A --> E[Table]

    B --> F[統計データ表示]
    C --> G[アクション実行]
    D --> H[ステータス表示]
    E --> I[データ一覧]

必要なコンポーネントを一括で追加します:

bash# 必要なコンポーネントを一括追加
npx shadcn-ui@latest add card button badge table

レイアウト構築

まず、ダッシュボードの基本レイアウトを構築します。現代的なダッシュボードでは、統計カードとデータテーブルを組み合わせたレイアウトが一般的ですね。

typescript// app/dashboard/page.tsx
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';

export default function DashboardPage() {
  return (
    <div className='container mx-auto p-6 space-y-6'>
      {/* ページヘッダー */}
      <div className='flex justify-between items-center'>
        <div>
          <h1 className='text-3xl font-bold'>
            ダッシュボード
          </h1>
          <p className='text-muted-foreground'>
            プロジェクトの概要と最新情報
          </p>
        </div>
        <Button>新規作成</Button>
      </div>

      {/* 統計カードエリア */}
      <div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6'>
        {/* 統計カード1 */}
        <Card>
          <CardHeader className='pb-2'>
            <CardTitle className='text-sm font-medium'>
              総売上
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className='text-2xl font-bold'>
              ¥1,234,567
            </div>
            <p className='text-xs text-muted-foreground'>
              前月比 +20.1%
            </p>
          </CardContent>
        </Card>

        {/* 統計カード2 */}
        <Card>
          <CardHeader className='pb-2'>
            <CardTitle className='text-sm font-medium'>
              新規ユーザー
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className='text-2xl font-bold'>+2,350</div>
            <p className='text-xs text-muted-foreground'>
              前月比 +180.1%
            </p>
          </CardContent>
        </Card>

        {/* 統計カード3 */}
        <Card>
          <CardHeader className='pb-2'>
            <CardTitle className='text-sm font-medium'>
              アクティブユーザー
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className='text-2xl font-bold'>12,234</div>
            <p className='text-xs text-muted-foreground'>
              前月比 +19%
            </p>
          </CardContent>
        </Card>

        {/* 統計カード4 */}
        <Card>
          <CardHeader className='pb-2'>
            <CardTitle className='text-sm font-medium'>
              コンバージョン率
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className='text-2xl font-bold'>3.2%</div>
            <p className='text-xs text-muted-foreground'>
              前月比 +1%
            </p>
          </CardContent>
        </Card>
      </div>
    </div>
  );
}

データバインディング

実際のプロジェクトでは、API からデータを取得してダッシュボードに表示します。Next.js の App Router を使った非同期データ取得の例を見てみましょう。

typescript// app/dashboard/page.tsx - データ取得版
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from '@/components/ui/card';
import {
  Table,
  TableBody,
  TableCell,
  TableHead,
  TableHeader,
  TableRow,
} from '@/components/ui/table';
import { Badge } from '@/components/ui/badge';

// 統計データの型定義
interface StatsData {
  totalRevenue: number;
  newUsers: number;
  activeUsers: number;
  conversionRate: number;
}

// ユーザーデータの型定義
interface UserData {
  id: string;
  name: string;
  email: string;
  status: 'active' | 'inactive' | 'pending';
  joinDate: string;
}

データ取得関数を作成します:

typescript// 統計データを取得する関数
async function getStatsData(): Promise<StatsData> {
  // 実際のAPIコール
  const response = await fetch('/api/stats', {
    cache: 'no-store', // 常に最新データを取得
  });

  if (!response.ok) {
    throw new Error('統計データの取得に失敗しました');
  }

  return response.json();
}

// ユーザーデータを取得する関数
async function getUsersData(): Promise<UserData[]> {
  const response = await fetch('/api/users', {
    cache: 'no-store',
  });

  if (!response.ok) {
    throw new Error('ユーザーデータの取得に失敗しました');
  }

  return response.json();
}

取得したデータを使用したダッシュボードコンポーネント:

typescriptexport default async function DashboardPage() {
  // 並列でデータを取得
  const [statsData, usersData] = await Promise.all([
    getStatsData(),
    getUsersData(),
  ]);

  return (
    <div className='container mx-auto p-6 space-y-6'>
      {/* 統計カードエリア - データバインディング */}
      <div className='grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6'>
        <Card>
          <CardHeader className='pb-2'>
            <CardTitle className='text-sm font-medium'>
              総売上
            </CardTitle>
          </CardHeader>
          <CardContent>
            <div className='text-2xl font-bold'>
              ¥{statsData.totalRevenue.toLocaleString()}
            </div>
          </CardContent>
        </Card>
        {/* 他の統計カードも同様に実装 */}
      </div>

      {/* ユーザーテーブル */}
      <Card>
        <CardHeader>
          <CardTitle>最新ユーザー</CardTitle>
          <CardDescription>
            最近登録されたユーザーの一覧です
          </CardDescription>
        </CardHeader>
        <CardContent>
          <Table>
            <TableHeader>
              <TableRow>
                <TableHead>名前</TableHead>
                <TableHead>メールアドレス</TableHead>
                <TableHead>ステータス</TableHead>
                <TableHead>登録日</TableHead>
              </TableRow>
            </TableHeader>
            <TableBody>
              {usersData.map((user) => (
                <TableRow key={user.id}>
                  <TableCell className='font-medium'>
                    {user.name}
                  </TableCell>
                  <TableCell>{user.email}</TableCell>
                  <TableCell>
                    <Badge
                      variant={
                        user.status === 'active'
                          ? 'default'
                          : user.status === 'pending'
                          ? 'secondary'
                          : 'destructive'
                      }
                    >
                      {user.status}
                    </Badge>
                  </TableCell>
                  <TableCell>{user.joinDate}</TableCell>
                </TableRow>
              ))}
            </TableBody>
          </Table>
        </CardContent>
      </Card>
    </div>
  );
}

このように、shadcn/ui のコンポーネントと Next.js の機能を組み合わせることで、わずか 30 分程度で本格的なダッシュボード画面を構築できました。

図で理解できる要点:

  • コンポーネントは用途別に整理されている
  • データの流れが明確で追跡しやすい
  • TypeScript による型安全性が確保されている

まとめ

shadcn/ui と Next.js の組み合わせは、モダンな Web 開発において革命的なソリューションだと言えるでしょう。従来の UI ライブラリが抱えていた課題を、Copy & Paste という斬新なアプローチで解決しています。

今回の記事でご紹介した内容を簡潔にまとめますと:

shadcn/ui の主なメリット

  • コンポーネントの完全な所有権とカスタマイズ性
  • TypeScript 完全対応による開発効率の向上
  • Next.js との完璧な統合

実践で得られる効果

  • UI 開発時間の大幅短縮(従来の 1/3 程度)
  • デザインシステムの統一性確保
  • メンテナンスコストの削減

特に印象深いのは、わずか 30 分でプロフェッショナルなダッシュボード画面を構築できた点です。これまで数日かかっていた作業が、shadcn/ui を使うことで劇的に短縮されました。

初心者の方でも、今回の手順に従って実際に手を動かしていただければ、きっと shadcn/ui × Next.js の素晴らしさを実感していただけることでしょう。

現代の Web 開発では、技術選択が成功の鍵を握ります。shadcn/ui と Next.js の組み合わせは、確実にあなたの開発体験を向上させてくれるはずです。

関連リンク