Next.jsでShadCN UIを統合する方法
 
  モダンなUIコンポーネントライブラリのセットアップは、説明書なしで家具を組み立てるような感覚であってはいけません。Next.js 15でビルドしており、従来のライブラリのブラックボックスな制約なしに美しくカスタマイズ可能なコンポーネントが欲しい場合、このガイドではShadcn UIをプロジェクトに統合する正確な方法を説明します。
この記事では、初期プロジェクト作成からコンポーネントのインストール、Tailwind CSS設定、適切なダークモード実装まで、完全なShadcn UI Next.jsセットアッププロセスをカバーします。一般的なReact 19互換性の問題を回避し、実際に所有できる本番環境対応のコンポーネントシステムをセットアップする方法を学習できます。
重要なポイント
- Shadcn UIはコンポーネントのソースコードを直接プロジェクトにコピーするため、完全な所有権とカスタマイズ制御が可能
- CLIがインストールと依存関係管理を処理するため、React 19互換性の考慮事項があってもセットアップが簡単
- Tailwind CSS統合はシームレスで、組み込みテーマサポートとダークモード実装を提供
- コンポーネントはフレームワーク非依存で、Next.jsのApp RouterとPages Routerの両方で動作
Next.jsプロジェクトでShadcn UIを選ぶ理由
Shadcn UIはコンポーネントライブラリに対して根本的に異なるアプローチを取ります。変更できない依存関係をインストールする代わりに、Shadcnはコンポーネントのソースコードを直接プロジェクトにコピーします。これにより以下が実現できます:
- コンポーネントの完全な所有権 - いつでも何でも変更可能
- Tailwindファーストデザイン - 既存のスタイルとシームレスに統合
- ランタイムオーバーヘッドゼロ - コンポーネントはプロジェクト内の単なるコード
- 型安全性 - 最初から組み込まれている
- アクセシビリティ - Radix UIプリミティブによってデフォルトで適切に処理
このアプローチは特にNext.js 15のApp Routerで優れており、サーバーコンポーネントとクライアント境界で慎重なコンポーネント設計が必要です。
前提条件と初期セットアップ
Shadcn UI Next.jsセットアップを開始する前に、以下を確認してください:
- Node.js 18.17以上
- パッケージマネージャー(npm、pnpm、yarn、またはbun)
- ReactとTailwind CSSの基本的な知識
Next.js 15プロジェクトの作成
TypeScriptとTailwind CSSを使用して新しいNext.jsプロジェクトを作成することから始めます:
npx create-next-app@latest my-app --typescript --tailwind --appプロンプトが表示されたら、以下のオプションを選択してください:
- ✓ Would you like to use ESLint? Yes
- ✓ Would you like to use src/directory? Yes(推奨)
- ✓ Would you like to customize the default import alias? No
プロジェクトに移動します:
cd my-appShadcn UIのインストールと設定
Shadcn CLIはコンポーネントインストールの大部分の作業を処理します。初期化コマンドを実行します:
npx shadcn@latest initいくつかの設定プロンプトが表示されます:
- Style:好みのスタイル(Default/New York)を選択
- Base color:利用可能なカラースキームから選択
- CSS variables:より良いテーマサポートのためYesを使用
React 19ピア依存関係の処理
React 19でnpmを使用している場合、ピア依存関係の警告が発生します:
npm error code ERESOLVE
npm error ERESOLVE unable to resolve dependency treeCLIは解決戦略を選択するよう促します:
? How would you like to proceed?
❯ Use --force
  Use --legacy-peer-deps最も安全なアプローチとして--legacy-peer-depsを選択してください。pnpm、yarn、またはbunのユーザーはこれらの警告を見ることなく、通常通り進めることができます。
最初のコンポーネントの追加
Shadcnが初期化されたら、CLIを使用してコンポーネントを追加します:
npx shadcn@latest add button cardこのコマンドは:
- コンポーネントのソースコードをダウンロード
- src/components/ui/に配置
- 必要な依存関係(Radix UIプリミティブなど)をインストール
- コンポーネントのインポートを更新
インストール後の典型的なShadcnボタンコンポーネントは以下のようになります:
// src/components/ui/button.tsx
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 transition-colors focus-visible:outline-none focus-visible:ring-2",
  {
    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",
        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",
    },
  }
)
export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"
    return (
      <Comp
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = "Button"
export { Button, buttonVariants }コンポーネントが完全にカスタマイズ可能であることに注目してください - バリアントを変更したり、新しいものを追加したり、スタイリングを完全に変更したりできます。
ShadcnのためのTailwind CSS設定
Shadcn UIはTailwind CSS v3とv4の両方で動作します。CLIは初期化中にtailwind.config.jsを自動的に設定します。以下が追加されます:
// tailwind.config.js
module.exports = {
  darkMode: ["class"],
  content: [
    './pages/**/*.{ts,tsx}',
    './components/**/*.{ts,tsx}',
    './app/**/*.{ts,tsx}',
    './src/**/*.{ts,tsx}',
  ],
  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)",
      },
    },
  },
  plugins: [require("tailwindcss-animate")],
}globals.cssの更新
CLIはテーマ用の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: 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%;
    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;
    --popover: 222.2 84% 4.9%;
    --popover-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%;
  }
}
@layer base {
  * {
    @apply border-border;
  }
  body {
    @apply bg-background text-foreground;
  }
}next-themesでダークモードの実装
本番環境対応のダークモード実装には、ハイドレーション問題を回避するためにnext-themesを使用します。
next-themesのインストール
npm install next-themesテーマプロバイダーの作成
src/components/theme-provider.tsxを作成します:
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}ルートレイアウトの更新
src/app/layout.tsxを変更します:
import { ThemeProvider } from "@/components/theme-provider"
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body>
        <ThemeProvider
          attribute="class"
          defaultTheme="system"
          enableSystem
          disableTransitionOnChange
        >
          {children}
        </ThemeProvider>
      </body>
    </html>
  )
}テーマトグルコンポーネントの作成
"use client"
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-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">テーマを切り替え</span>
    </Button>
  )
}React 19の変更への対応
React 19はShadcnコンポーネントでの作業方法に影響する複数の変更を導入します:
簡素化されたref処理
React 19ではrefを通常のプロパティとして渡すことができ、多くの場合forwardRefの必要性がなくなります:
// 古いアプローチ(React 18)
const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, ...props }, ref) => {
    return <input ref={ref} className={className} {...props} />
  }
)
// 新しいアプローチ(React 19)
function Input({ className, ref, ...props }: InputProps & { ref?: React.Ref<HTMLInputElement> }) {
  return <input ref={ref} className={className} {...props} />
}新しいフォームフック
React 19のuseActionStateとuseFormStatusフックはShadcnフォームコンポーネントとシームレスに動作します:
import { useActionState } from "react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
function ContactForm() {
  const [state, formAction] = useActionState(async (prevState, formData) => {
    // サーバーアクションロジックをここに
    const email = formData.get("email")
    // フォームを処理...
    return { success: true }
  }, { success: false })
  return (
    <form action={formAction}>
      <Input name="email" type="email" required />
      <Button type="submit">送信</Button>
      {state.success && <p>ご登録ありがとうございます!</p>}
    </form>
  )
}よくある問題と解決策
Tailwind CSSでのビルドエラー
セットアップ後にビルドエラーが発生する場合、PostCSS設定が正しいことを確認してください:
// postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
}コンポーネントスタイリングが適用されない
globals.cssがルートレイアウトでインポートされていることを確認してください:
import "./globals.css"コンポーネントプロパティでのTypeScriptエラー
tsconfig.jsonに正しいパスが含まれていることを確認してください:
{
  "compilerOptions": {
    "paths": {
      "@/*": ["./src/*"]
    }
  }
}本番環境でのベストプラクティス
- 
cnユーティリティのカスタマイズ: lib/utils.tsのcn関数はクラスを賢く結合します。特定のニーズに合わせて拡張してください。
- 
コンポーネントバリアントの作成:CVA(class-variance-authority)を使用してアプリケーション全体で一貫したコンポーネントバリアントを作成してください。 
- 
バンドルサイズの最適化:実際に使用するコンポーネントのみをインストールしてください。各コンポーネントは独立しています。 
- 
アクセシビリティのテスト:Shadcnコンポーネントはアクセシビリティを処理するRadix UIプリミティブを使用していますが、常にスクリーンリーダーでテストしてください。 
- 
バージョン管理:コンポーネントはプロジェクトにコピーされるため、バージョン管理にコミットし、変更を追跡してください。 
結論
Next.jsでShadcn UIをセットアップすることで、柔軟性と開発速度のバランスを取るモダンなコンポーネントシステムが得られます。従来のコンポーネントライブラリとは異なり、すべてのコード行を所有し、何でもカスタマイズでき、他人の設計決定に縛られることがありません。
Next.js 15のApp Router、React 19の改善、Tailwind CSSの組み合わせは、モダンなWebアプリケーション構築のための強力な基盤を作成します。依存関係としてインストールするのではなくコンポーネントをコピーするShadcnのアプローチにより、迅速な開発と完全な制御という両方の世界の最良の部分を得ることができます。
FAQ
React 19でnpmを使用する場合、ピア依存関係の競合のため、コンポーネントインストール中に--legacy-peer-depsフラグを使用する必要があります。pnpm、yarn、bunなどのパッケージマネージャーは、これらの依存関係をより優雅に処理し、特別なフラグを必要としません。どのパッケージマネージャーを選択しても、最終結果は同じです。
はい、Shadcn UIはApp RouterとPages Routerの両方で動作します。コンポーネントコード自体はフレームワーク非依存です。主な違いは、テーマプロバイダーなどのプロバイダーの実装方法にあります - Pages Routerでは、ルートレイアウトの代わりに_app.tsxでアプリをラップします。
Shadcnはコンポーネントを直接プロジェクトにコピーするため、他のコードと同様に変更できます。src/components/ui/のコンポーネントファイルを開き、TailwindクラスやCVAバリアントを調整して保存します。また、CSSのグローバルテーマ変数を変更して、すべてのコンポーネント全体で色や間隔を一度に変更することもできます。
いいえ、必要に応じてコンポーネントのみをインストールする必要があります。各コンポーネントは独立しており、独自の依存関係を含んでいます。これによりバンドルサイズを最小限に抑え、プロジェクトを整理された状態に保ちます。必要に応じてnpx shadcn@latest add [component-name]を使用して個別のコンポーネントを追加してください。
コンポーネントはプロジェクトにコピーされるため、Shadcnが新しいバージョンをリリースしても自動的に更新されません。Shadcn UIドキュメントで更新を手動で確認し、特定のコンポーネントに対してaddコマンドを再実行するか、手動で変更を適用できます。これにより、プロジェクトでコンポーネントがいつ、どのように更新されるかを制御できます。
