Documentation Next.js

はじめに

この記事では、Next.jsとTailwind CSSを組み合わせて効率的にWebアプリケーションを構築する方法を解説します。

Tailwind CSSは「ユーティリティファースト」というアプローチを採用したCSSフレームワークです。従来のCSSフレームワーク(Bootstrapなど)とは異なり、あらかじめ定義された小さなユーティリティクラスを組み合わせてスタイルを構築します。

Next.jsのサーバーサイドレンダリング(SSR)や静的サイト生成(SSG)と組み合わせることで、パフォーマンスに優れたモダンなWebアプリケーションを構築できます。

Tailwind CSSとは

Tailwind CSSは、以下の特徴を持つCSSフレームワークです。

特徴説明
ユーティリティファーストflexpt-4text-center などの小さなクラスを組み合わせる
カスタマイズ性設定ファイルで色、フォント、スペーシングなどを自由に定義可能
レスポンシブ対応sm:, md:, lg: などのプレフィックスで簡単にレスポンシブデザイン
本番環境の最適化使用していないCSSを自動的に削除してファイルサイズを最小化

Tailwind CSSの導入方法

方法1: create-next-appで新規プロジェクト作成(推奨)

最も簡単な方法は、プロジェクト作成時にTailwind CSSを選択することです。

# 対話形式でプロジェクトを作成
npx create-next-app@latest my-app

対話形式で以下の質問が表示されるので、Tailwind CSSを選択します。

Would you like to use Tailwind CSS? › Yes

この方法では、Tailwind CSSの設定が自動的に完了します。

方法2: 既存プロジェクトへの導入

既存のNext.jsプロジェクトにTailwind CSSを追加する場合は、以下の手順で行います。

1. パッケージのインストール

# Tailwind CSS と関連パッケージをインストール
npm install -D tailwindcss postcss autoprefixer
  • tailwindcss: Tailwind CSS本体
  • postcss: CSSの変換処理を行うツール
  • autoprefixer: ベンダープレフィックスを自動付与

2. 設定ファイルの生成

# tailwind.config.js と postcss.config.js を生成
npx tailwindcss init -p

3. tailwind.config.jsの設定

生成された tailwind.config.js を編集し、コンテンツのパスを設定します。

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  // Tailwindが使用されているファイルのパスを指定
  // これにより未使用のCSSが本番ビルドから除外される
  content: [
    './app/**/*.{js,ts,jsx,tsx,mdx}',      // App Router使用時
    './pages/**/*.{js,ts,jsx,tsx,mdx}',    // Pages Router使用時
    './components/**/*.{js,ts,jsx,tsx,mdx}',
    './src/**/*.{js,ts,jsx,tsx,mdx}',      // srcディレクトリ使用時
  ],
  theme: {
    extend: {
      // カスタムカラーやフォントなどはここで拡張
    },
  },
  plugins: [],
}

4. グローバルCSSの設定

app/globals.css(App Router)または styles/globals.css(Pages Router)に、Tailwindのディレクティブを追加します。

/* globals.css */

/* Tailwindの基本スタイル(リセットCSSなど) */
@tailwind base;

/* Tailwindのコンポーネントクラス */
@tailwind components;

/* Tailwindのユーティリティクラス */
@tailwind utilities;

5. CSSファイルのインポート

App Router(app/layout.tsx)の場合:

// app/layout.tsx
import './globals.css'

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="ja">
      <body>{children}</body>
    </html>
  )
}

Pages Router(pages/_app.tsx)の場合:

// pages/_app.tsx
import '../styles/globals.css'
import type { AppProps } from 'next/app'

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}

基本的な使い方

ユーティリティクラスの組み合わせ

Tailwind CSSでは、HTMLの className 属性にユーティリティクラスを記述してスタイルを適用します。

// components/Card.tsx
export function Card() {
  return (
    <div className="
      bg-white          // 背景色: 白
      rounded-lg        // 角丸: large
      shadow-md         // 影: medium
      p-6               // padding: 1.5rem (24px)
      hover:shadow-lg   // ホバー時: 影を大きく
      transition-shadow // 影の変化をアニメーション
    ">
      <h2 className="
        text-xl           // フォントサイズ: 1.25rem
        font-bold         // フォントウェイト: 太字
        text-gray-800     // 文字色: グレー800
        mb-2              // margin-bottom: 0.5rem
      ">
        カードタイトル
      </h2>
      <p className="text-gray-600">
        カードの説明文がここに入ります。
      </p>
    </div>
  )
}

よく使うユーティリティクラス

カテゴリクラス例説明
レイアウトflex, grid, block表示方法
幅・高さw-full, h-screen, max-w-mdサイズ指定
スペーシングp-4, m-2, space-x-4余白
bg-blue-500, text-white背景色・文字色
タイポグラフィtext-lg, font-boldフォント
ボーダーborder, rounded-lg枠線・角丸
効果shadow-md, opacity-50影・透明度

レスポンシブデザイン

Tailwind CSSは、ブレークポイントのプレフィックスを使って簡単にレスポンシブデザインを実現できます。

ブレークポイント一覧

プレフィックス最小幅対象デバイス
sm:640px小型タブレット
md:768pxタブレット
lg:1024pxノートPC
xl:1280pxデスクトップ
2xl:1536px大型ディスプレイ

レスポンシブレイアウトの実装例

// components/ResponsiveGrid.tsx
export function ResponsiveGrid() {
  return (
    <div className="
      grid                  // グリッドレイアウト
      grid-cols-1           // デフォルト: 1列
      sm:grid-cols-2        // 640px以上: 2列
      lg:grid-cols-3        // 1024px以上: 3列
      xl:grid-cols-4        // 1280px以上: 4列
      gap-4                 // グリッド間隔: 1rem
      p-4                   // padding: 1rem
    ">
      {[1, 2, 3, 4, 5, 6].map((item) => (
        <div
          key={item}
          className="
            bg-blue-500
            text-white
            p-6
            rounded-lg
            text-center
          "
        >
          アイテム {item}
        </div>
      ))}
    </div>
  )
}

レスポンシブ画像の実装

// components/ResponsiveImage.tsx
import Image from 'next/image'

export function ResponsiveImage() {
  return (
    <div className="
      w-full              // 幅: 100%
      md:w-1/2            // 768px以上: 幅50%
      lg:w-1/3            // 1024px以上: 幅33%
      mx-auto             // 左右中央揃え
    ">
      <Image
        src="/sample.jpg"
        alt="サンプル画像"
        width={800}
        height={600}
        className="
          w-full
          h-auto
          rounded-lg
          shadow-md
        "
      />
    </div>
  )
}

ダークモード対応

Tailwind CSSでは dark: プレフィックスを使用してダークモードのスタイルを定義できます。

設定

// tailwind.config.js
module.exports = {
  // 'media': OSの設定に従う, 'class': クラスで制御
  darkMode: 'class',
  content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
  // ...
}

ダークモード対応コンポーネント

// components/DarkModeCard.tsx
export function DarkModeCard() {
  return (
    <div className="
      bg-white              // ライトモード: 白背景
      dark:bg-gray-800      // ダークモード: グレー背景
      text-gray-900         // ライトモード: 濃いグレー文字
      dark:text-gray-100    // ダークモード: 薄いグレー文字
      p-6
      rounded-lg
      shadow-md
      dark:shadow-gray-900/50  // ダークモード用の影
    ">
      <h2 className="
        text-xl
        font-bold
        text-blue-600
        dark:text-blue-400
      ">
        ダークモード対応
      </h2>
      <p className="mt-2">
        OSの設定やクラスに応じて自動的にスタイルが切り替わります。
      </p>
    </div>
  )
}

ダークモード切り替えボタン

// components/ThemeToggle.tsx
'use client'
import { useState, useEffect } from 'react'

export function ThemeToggle() {
  const [isDark, setIsDark] = useState(false)

  useEffect(() => {
    // 初期状態をlocalStorageから取得
    const saved = localStorage.getItem('theme')
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
    setIsDark(saved === 'dark' || (!saved && prefersDark))
  }, [])

  useEffect(() => {
    // テーマの切り替え
    if (isDark) {
      document.documentElement.classList.add('dark')
      localStorage.setItem('theme', 'dark')
    } else {
      document.documentElement.classList.remove('dark')
      localStorage.setItem('theme', 'light')
    }
  }, [isDark])

  return (
    <button
      onClick={() => setIsDark(!isDark)}
      className="
        p-2
        rounded-lg
        bg-gray-200
        dark:bg-gray-700
        hover:bg-gray-300
        dark:hover:bg-gray-600
        transition-colors
      "
    >
      {isDark ? '🌙' : '☀️'}
    </button>
  )
}

カスタマイズ

カスタムカラーの追加

// tailwind.config.js
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
  theme: {
    extend: {
      // カスタムカラーを追加
      colors: {
        // ブランドカラー
        brand: {
          50: '#f0fdf4',
          100: '#dcfce7',
          500: '#22c55e',
          600: '#16a34a',
          700: '#15803d',
        },
        // 単色
        primary: '#3490dc',
        secondary: '#ffed4a',
      },
    },
  },
}

使用例:

<button className="bg-brand-500 hover:bg-brand-600 text-white">
  ブランドカラーボタン
</button>

カスタムフォント

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      fontFamily: {
        // 日本語フォント
        sans: ['Noto Sans JP', 'sans-serif'],
        // 見出し用
        heading: ['Poppins', 'sans-serif'],
      },
    },
  },
}

カスタムスペーシング

// tailwind.config.js
module.exports = {
  theme: {
    extend: {
      spacing: {
        '128': '32rem',
        '144': '36rem',
      },
    },
  },
}

パフォーマンス最適化

1. contentオプションの適切な設定

Tailwind CSS v3以降では、content オプションに指定されたファイルをスキャンし、使用されているクラスのみをCSSに含めます。

// tailwind.config.js
module.exports = {
  content: [
    // 必要なパスのみ指定(不要なパスは含めない)
    './src/components/**/*.{js,ts,jsx,tsx}',
    './src/app/**/*.{js,ts,jsx,tsx}',
    // node_modules内のライブラリを使う場合
    './node_modules/@company/ui/**/*.js',
  ],
}

2. JITモード(Just-In-Time)

Tailwind CSS v3ではJITモードがデフォルトで有効になっています。JITモードの特徴は以下の通りです。

特徴説明
高速なビルド使用するクラスのみを生成するため高速
任意の値w-[123px] のような任意の値が使用可能
スタック可能な修飾子sm:hover:active:disabled:opacity-50 など
小さなCSSファイル未使用クラスが含まれない

3. 任意の値(Arbitrary Values)

JITモードでは、ブラケット記法で任意の値を使用できます。

<div className="
  w-[calc(100%-2rem)]  // 任意の幅計算
  h-[500px]            // 任意の高さ
  bg-[#1da1f2]         // 任意のカラーコード
  grid-cols-[200px_1fr_200px]  // 任意のグリッド
">
  コンテンツ
</div>

4. 不要なプラグインの削除

使用しないプラグインを無効化することで、ビルドサイズを削減できます。

// tailwind.config.js
module.exports = {
  corePlugins: {
    // 使わない機能を無効化
    float: false,
    objectFit: false,
    objectPosition: false,
  },
}

実践的なコンポーネント例

ナビゲーションバー

// components/Navbar.tsx
'use client'
import Link from 'next/link'
import { useState } from 'react'

export function Navbar() {
  const [isOpen, setIsOpen] = useState(false)

  return (
    <nav className="bg-white shadow-md dark:bg-gray-900">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="flex justify-between h-16">
          {/* ロゴ */}
          <div className="flex items-center">
            <Link href="/" className="text-xl font-bold text-blue-600">
              MyApp
            </Link>
          </div>

          {/* デスクトップメニュー */}
          <div className="hidden md:flex items-center space-x-8">
            <Link href="/about" className="text-gray-700 hover:text-blue-600 dark:text-gray-300">
              About
            </Link>
            <Link href="/services" className="text-gray-700 hover:text-blue-600 dark:text-gray-300">
              Services
            </Link>
            <Link href="/contact" className="text-gray-700 hover:text-blue-600 dark:text-gray-300">
              Contact
            </Link>
          </div>

          {/* モバイルメニューボタン */}
          <div className="md:hidden flex items-center">
            <button
              onClick={() => setIsOpen(!isOpen)}
              className="text-gray-700 dark:text-gray-300"
            >
              <svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
                {isOpen ? (
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
                ) : (
                  <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
                )}
              </svg>
            </button>
          </div>
        </div>
      </div>

      {/* モバイルメニュー */}
      {isOpen && (
        <div className="md:hidden">
          <div className="px-2 pt-2 pb-3 space-y-1">
            <Link href="/about" className="block px-3 py-2 text-gray-700 hover:bg-gray-100 dark:text-gray-300">
              About
            </Link>
            <Link href="/services" className="block px-3 py-2 text-gray-700 hover:bg-gray-100 dark:text-gray-300">
              Services
            </Link>
            <Link href="/contact" className="block px-3 py-2 text-gray-700 hover:bg-gray-100 dark:text-gray-300">
              Contact
            </Link>
          </div>
        </div>
      )}
    </nav>
  )
}

フォームコンポーネント

// components/ContactForm.tsx
'use client'
import { useState } from 'react'

export function ContactForm() {
  const [formData, setFormData] = useState({
    name: '',
    email: '',
    message: '',
  })

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    console.log(formData)
  }

  return (
    <form onSubmit={handleSubmit} className="max-w-md mx-auto p-6">
      {/* 名前入力 */}
      <div className="mb-4">
        <label
          htmlFor="name"
          className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
        >
          お名前
        </label>
        <input
          type="text"
          id="name"
          value={formData.name}
          onChange={(e) => setFormData({ ...formData, name: e.target.value })}
          className="
            w-full
            px-3
            py-2
            border
            border-gray-300
            rounded-md
            shadow-sm
            focus:outline-none
            focus:ring-2
            focus:ring-blue-500
            focus:border-blue-500
            dark:bg-gray-800
            dark:border-gray-600
            dark:text-white
          "
          required
        />
      </div>

      {/* メールアドレス入力 */}
      <div className="mb-4">
        <label
          htmlFor="email"
          className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
        >
          メールアドレス
        </label>
        <input
          type="email"
          id="email"
          value={formData.email}
          onChange={(e) => setFormData({ ...formData, email: e.target.value })}
          className="
            w-full
            px-3
            py-2
            border
            border-gray-300
            rounded-md
            shadow-sm
            focus:outline-none
            focus:ring-2
            focus:ring-blue-500
            focus:border-blue-500
            dark:bg-gray-800
            dark:border-gray-600
            dark:text-white
          "
          required
        />
      </div>

      {/* メッセージ入力 */}
      <div className="mb-6">
        <label
          htmlFor="message"
          className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
        >
          メッセージ
        </label>
        <textarea
          id="message"
          rows={4}
          value={formData.message}
          onChange={(e) => setFormData({ ...formData, message: e.target.value })}
          className="
            w-full
            px-3
            py-2
            border
            border-gray-300
            rounded-md
            shadow-sm
            focus:outline-none
            focus:ring-2
            focus:ring-blue-500
            focus:border-blue-500
            dark:bg-gray-800
            dark:border-gray-600
            dark:text-white
            resize-none
          "
          required
        />
      </div>

      {/* 送信ボタン */}
      <button
        type="submit"
        className="
          w-full
          py-2
          px-4
          bg-blue-600
          text-white
          font-medium
          rounded-md
          hover:bg-blue-700
          focus:outline-none
          focus:ring-2
          focus:ring-blue-500
          focus:ring-offset-2
          transition-colors
        "
      >
        送信する
      </button>
    </form>
  )
}

まとめ

Next.jsとTailwind CSSを組み合わせることで、以下のメリットが得られます。

  1. 開発効率の向上: ユーティリティクラスを使った高速なスタイリング
  2. レスポンシブデザイン: プレフィックスによる簡単なブレークポイント対応
  3. ダークモード: dark: プレフィックスによる簡単なテーマ切り替え
  4. パフォーマンス最適化: JITモードによる最小限のCSSファイル生成
  5. カスタマイズ性: 設定ファイルによる柔軟なデザインシステム構築

特にJITモードの活用と適切な content 設定により、本番環境で最適化されたCSSを生成できます。

参考文献

円