はじめに
この記事では、Next.jsとTailwind CSSを組み合わせて効率的にWebアプリケーションを構築する方法を解説します。
Tailwind CSSは「ユーティリティファースト」というアプローチを採用したCSSフレームワークです。従来のCSSフレームワーク(Bootstrapなど)とは異なり、あらかじめ定義された小さなユーティリティクラスを組み合わせてスタイルを構築します。
Next.jsのサーバーサイドレンダリング(SSR)や静的サイト生成(SSG)と組み合わせることで、パフォーマンスに優れたモダンなWebアプリケーションを構築できます。
Tailwind CSSとは
Tailwind CSSは、以下の特徴を持つCSSフレームワークです。
| 特徴 | 説明 |
|---|---|
| ユーティリティファースト | flex、pt-4、text-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を組み合わせることで、以下のメリットが得られます。
- 開発効率の向上: ユーティリティクラスを使った高速なスタイリング
- レスポンシブデザイン: プレフィックスによる簡単なブレークポイント対応
- ダークモード:
dark:プレフィックスによる簡単なテーマ切り替え - パフォーマンス最適化: JITモードによる最小限のCSSファイル生成
- カスタマイズ性: 設定ファイルによる柔軟なデザインシステム構築
特にJITモードの活用と適切な content 設定により、本番環境で最適化されたCSSを生成できます。