👁

Next.jsのスタイリング

この章では、Next.jsでのスタイリングについて概要を解説し、ハンズオン形式で実際に動作させながら学習します。これにより、グローバルCSS・CSS Modules・Tailwind CSSの使い分けとレスポンシブデザインの基本が身につきます。

1. 本章の概要

1.1 本章の目的

Next.jsアプリケーションの見た目は複数のスタイリング手法を組み合わせて作ります。手法ごとにスコープの効き方や設計上の得意領域が異なるため、選び方を最初に押さえておくと、後の実装で迷いにくくなります。本章では、App Routerで追加設定なしに使えるグローバルCSS・CSS Modules・Tailwind CSSの位置づけと使い分けに加えて、レスポンシブデザインの基本、テーマ・カラーパレットの扱いも扱います。

1.2 ハンズオンの流れ

本章専用のNext.jsプロジェクトを新規作成し、グローバルCSS・CSS Modules・Tailwind CSSを段階的に適用しながら、同じUIをそれぞれの手法で書いた場合の差を比較します。仕上げにレスポンシブ対応も加えます。

1.3 事前準備

必要なツール

この章では、以下のツールを使用します。まだインストールしていない場合は、リンク先の手順に沿って準備をお願いします。

ツール名 関連箇所 理由
Visual Studio Code Visual Studio Codeのインストール Next.jsアプリケーションのコードを記述するエディタとして使用する
Node.js Node.jsをインストールしよう Next.jsアプリケーションの実行環境として使用する

2. Next.jsでのスタイリング手法

前章のNext.jsの基本では、App Routerによるルーティングやコンポーネントの分割方法を学びました。ただしこれだけでは、画面は装飾のない状態のままです。ここからは、コンポーネントに見た目を与える「スタイリング」の手法を扱います。

Next.jsは特定のスタイリング手法を強制せず、標準的なCSSの仕組みを含めていくつかの選択肢を提供しています。本章では、よく採用される以下の3つを取り上げます。

手法 特徴 主な用途
グローバルCSS app/layout.tsxでimportし、全ページに適用する。従来のCSSと同じ書き方ができる リセットCSSやbodyのフォント・背景色などサイト全体の共通スタイルに使う
CSS Modules xxx.module.cssという命名規約のファイルを作り、クラス名が自動的にコンポーネント単位でスコープ化される コンポーネント単位でスタイルを閉じ込めたいときに使い、CSSの記述をそのまま活かせる
Tailwind CSS 事前に用意されたユーティリティクラス(p-4 text-red-500など)をHTMLに直接付ける 独自のクラス名を作らずに、素早く一貫したデザインを作りたいときに使う

どれか1つだけを選ばないといけないわけではなく、たとえば「全体の基礎スタイルはグローバルCSS、個別コンポーネントはCSS Modules」のように併用する構成も一般的です。

💡 ポイント
Next.jsのApp Routerでは、グローバルCSS・CSS Modules・Tailwind CSSの3つがいずれも公式にサポートされており、追加のバンドラ設定なしでそのまま使えます。
💡 ポイント
Reactのスタイリング手法にはこの3つ以外に、JavaScript内でスタイルを記述するCSS-in-JS(styled-components / Emotionなど)もあります。ただしNext.jsのApp Routerでは、Server ComponentでCSS-in-JSを使うために追加のセットアップが必要で、公式ドキュメントも「まだ完全にはサポートしていない」と案内しています。本章では、App Routerに追加設定なしでそのまま組み込めるグローバルCSS・CSS Modules・Tailwind CSSに絞って扱います。

3. プロジェクトの準備

ここからは本章専用のNext.jsプロジェクトを新規作成し、そこにスタイリングの3手法を段階的に適用していきます。前章のmy-next-appとは別のフォルダで進めることで、本章の内容を独立したハンズオンとして再現できるようにします。

3.1 プロジェクト用フォルダの作成

任意の場所に styling-practice フォルダを作成し、Visual Studio Codeの「ファイル」→「フォルダーを開く」から、作成した styling-practice フォルダを開きます。以降の操作は、Visual Studio Codeのターミナルから行います。

styling-practice  ← このフォルダを作成

3.2 create-next-appによるプロジェクト初期化

Visual Studio Codeで新しいターミナルを開き、以下のコマンドを実行します。末尾の.は「現在のフォルダにプロジェクトを作成する」という意味です。

npx create-next-app@latest .

初回実行時はcreate-next-appパッケージのインストール確認が表示されるので、yを入力して進めます。

Need to install the following packages:
create-next-app@x.x.x
Ok to proceed? (y)

続いてNext.jsのデフォルト構成を使うかを聞かれるので、Yes, use recommended defaultsを選択します。これでTypeScript・ESLint・Tailwind CSS・App Routerなど、Next.jsが推奨するセットアップが自動で有効になります。

? Would you like to use the recommended Next.js defaults? › Yes, use recommended defaults

回答が終わると、必要な依存パッケージが自動でインストールされ、いま開いているstyling-practiceフォルダに雛形が生成されます。以下のような実行結果が表示されれば成功です。

Success! Created styling-practice at /path/to/styling-practice

3.3 開発サーバの起動

雛形を生成しただけではブラウザで動作を確認できないため、開発サーバを起動します。開発サーバはコードの変更を検知して自動的にリビルド・再読み込みしてくれるため、以降の変更もこのサーバを起動したまま確認できます。

Visual Studio Codeのターミナルで、以下のコマンドを実行します。

npm run dev

以下のような実行結果が表示され、Ready in ... のメッセージが出れば起動完了です。

> styling-practice@0.1.0 dev
> next dev

   ▲ Next.js 16.x.x (Turbopack)
   - Local:        http://localhost:3000
   - Network:      http://192.168.x.x:3000

 ✓ Ready in 1.2s

ブラウザで http://localhost:3000 を開き、create-next-appの初期ページが表示されることを確認します。この画面が表示されれば、本章で使うNext.jsプロジェクトの準備は完了です。

💡 ポイント
create-next-appのバージョンによっては、雛形の生成直後にそのまま開発サーバが自動で起動する場合があります。その場合は改めてnpm run devを実行する必要はなく、そのまま次のステップに進んでください。
⚠️ ポート3000が使用中というエラーが出る場合
既に他のNext.jsプロジェクトの開発サーバが動いている可能性があります。その場合は自動的にhttp://localhost:3001など空いているポートが割り当てられるため、表示されたURLをそのまま使ってください。

4. グローバルCSS

グローバルCSSは、サイト全体に一律で適用したいスタイルを書く場所です。Next.jsのApp Routerでは、慣習としてapp/globals.cssにまとめ、これをapp/layout.tsxでimportします。

4.1 layout.tsx

app/layout.tsxは全ページの共通レイアウトを定義するファイルで、ここでimportされたCSSは全ページに読み込まれます。グローバルCSSを読み込むには、レイアウトファイルの先頭で相対パス指定の import 文を1行書きます。書き方は以下のとおりです。

import 'CSSファイルへのパス';

create-next-app で作成したプロジェクトの app/layout.tsx には、はじめから app/globals.css の import 文が含まれています。ここでは編集は不要で、Visual Studio Codeのエクスプローラーで app/layout.tsx を開き、import './globals.css'; の1行が先頭に入っていることを確認するだけで問題ありません。

import './globals.css';

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

コードを解説します。

import './globals.css';

app/layout.tsxと同じ階層にあるglobals.cssをimportしています。App Routerでは、ここでimportしたグローバルCSSがすべてのページに適用されます。

export default function RootLayout({ children }) {
  return (
    <html lang="ja">
      <body>{children}</body>
    </html>
  );
}

RootLayoutはすべてのページの外側を囲むレイアウトコンポーネントです。childrenには各ページのpage.tsxの内容が差し込まれます。<html> <body>もここに書くのがApp Routerの規約です。

4.2 globals.cssへのスタイル記述

app/globals.cssにサイト全体に適用したいスタイルを書きます。書き方は以下のとおりです。

セレクタ {
  プロパティ: 値;
}

create-next-app で生成された app/globals.css の先頭には、Tailwind CSS本体を読み込む @import "tailwindcss"; が入っています。この1行を消してしまうと、後述のTailwind CSSセクションでユーティリティクラスが効かなくなるため、必ず残したうえで本章で扱うスタイルを書きます。

以下は、@import "tailwindcss"; を残しつつ、既定で入っているCSS変数・ダークモード用メディアクエリ・bodyスタイルを本章で扱うフォント・背景色・行間・リンクの色に差し替えた app/globals.css の最終形です。Visual Studio Codeで app/globals.css を開き、既存の内容をすべて削除して以下の内容に置き換えます。

@import "tailwindcss";

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  color: #1f2937;
  background-color: #f9fafb;
  line-height: 1.6;
}

a {
  color: #2563eb;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

保存すると開発サーバが自動で再ビルドし、ブラウザで開いている http://localhost:3000 のトップページで、以下の変化を確認できます。

  • ページ全体の背景色が薄いグレー(#f9fafb)に変わる
  • フォントがシステム標準のサンセリフに切り替わる
  • 本文の行間が広がり、テキストが読みやすくなる

コードを解説します。

@import "tailwindcss";

create-next-app のデフォルトで先頭に入っているTailwind CSS本体を読み込む1行です。後述のTailwind CSSセクションで利用するため、そのまま残しておきます。

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  color: #1f2937;
  background-color: #f9fafb;
  line-height: 1.6;
}

bodyセレクタでページ全体のフォント・文字色・背景色・行間を指定しています。font-familyに複数のフォントを並べているのは、OSごとに標準搭載されるフォントに合わせて指定するためです。

a {
  color: #2563eb;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

リンク(<a>)の色と、下線の表示・非表示を指定しています。通常時は下線なしの青色で表示し、マウスカーソルを合わせたときだけ下線を付けることで、リンクであることを視認しやすくしています。

📝 リセットCSSとTailwind CSSのPreflight
ブラウザによって異なる要素の既定スタイル(<body>の外側マージン、<h1>の上下マージンなど)を打ち消し、どのブラウザでも同じ土台からスタイリングできるようにするための一連のスタイル指定をリセットCSS(またはNormalize CSS)と呼びます。本プロジェクトではTailwind CSSがこの役割を担うPreflightという層を提供しており、@import "tailwindcss"; を書いた時点でbox-sizing: border-boxの付与や見出し・段落・リストの既定マージンの解除などが自動で適用されます。そのため、本章のグローバルCSSには* { margin: 0; padding: 0; }のような追加のリセットは書いていません。Tailwindを使わないプロジェクトでは、modern-normalize(GitHub)などの汎用リセットCSSを別途導入することが一般的です。

4.3 グローバルCSSに置くスタイルの方針

グローバルCSSはあらゆるページ・コンポーネントに影響するため、書きすぎるとどこで定義されたスタイルなのか追いにくくなります。「全画面に一律で適用したいもの」だけをグローバルCSSに置き、個別のコンポーネントのスタイルは後述のCSS ModulesやTailwind CSSに寄せるのが基本方針です。

📝 グローバルCSSはApp Router配下でのみimport可能
App Routerでは、app/globals.cssのようなグローバルCSSはapp/layout.tsxなどapp/配下のファイルからのみimportできます。個別コンポーネントからimport './globals.css'のように読み込むと、Next.jsがビルド時にエラーとして検出します。個別のコンポーネントから読み込みたい場合は、CSS Modulesを利用します。

5. CSS Modules

グローバルCSSは、app/layout.tsxでimportするだけでサイト全体にスタイルを一括適用できる手軽さがある一方で、あらゆるページ・コンポーネントに影響が及ぶため、簡単にクラス名が衝突するような問題が起こりがちです。たとえば「Buttonコンポーネント用の.buttonクラスを定義したら、別のコンポーネントの.buttonと競合してスタイルが崩れた」といったケースが典型的です。CSS Modulesは、この「クラス名の衝突」を仕組みとして解消するための機能です。

5.1 ファイル名の規約とスコープ化

CSS Modulesを使うには、CSSファイルの名前を**xxx.module.css**という規約に沿ってつけます。この規約に従うと、Next.jsはビルド時にクラス名をbutton_button__aB12xのような一意な文字列へ自動的に変換します。この変換のおかげで、別のファイルで同じ.buttonというクラス名を書いても、実際に生成されるクラス名は別物になるため衝突しません。

CSSファイルの作成

Buttonコンポーネント用のCSSファイルを作成します。Visual Studio Codeのエクスプローラーで app フォルダを右クリックし、「新しいフォルダー」から components フォルダを作成します。次に、作成した components フォルダを右クリックして「新しいファイル」から Button.module.css を作成します。

styling-practice/
└── app/
    └── components/  ← このフォルダを作成
        └── Button.module.css  ← このファイルを作成

作成した Button.module.css に以下の内容を書き込みます。

.button {
  padding: 8px 16px;
  border: none;
  border-radius: 6px;
  background-color: #2563eb;
  color: #ffffff;
  font-size: 14px;
  cursor: pointer;
}

.button:hover {
  background-color: #1d4ed8;
}

tsxファイルへの適用

このCSSをTypeScript側から読み込みます。CSS Modulesを使うTypeScriptファイルでは、CSSファイルをオブジェクトとしてimportします。オブジェクトのプロパティ名が、CSS内で定義したクラス名と対応します。書き方は以下のとおりです。

import styles from './コンポーネント名.module.css';

export function コンポーネント名() {
  return <要素タグ className={styles.クラス名}>...</要素タグ>;
}

以下は、Button.module.css.button クラスをButtonコンポーネントに適用する例です。Visual Studio Codeのエクスプローラーで components フォルダを右クリックし、「新しいファイル」から Button.tsx を作成します。

styling-practice/
└── app/
    └── components/
        ├── Button.module.css
        └── Button.tsx  ← このファイルを作成

作成した Button.tsx に以下の内容を書き込みます。

'use client';

import styles from './Button.module.css';

type Props = {
  label: string;
  onClick?: () => void;
};

export function Button({ label, onClick }: Props) {
  return (
    <button className={styles.button} onClick={onClick}>
      {label}
    </button>
  );
}

画面への配置と動作確認

作成したButtonコンポーネントを画面に表示するため、app/page.tsx を以下の内容に置き換えます。

'use client';

import { Button } from './components/Button';

export default function Home() {
  return (
    <main style={{ padding: '32px' }}>
      <Button label="保存" onClick={() => alert('保存しました')} />
    </main>
  );
}

保存すると、ブラウザで開いている http://localhost:3000 の表示が置き換わり、create-next-app の初期ページが消えて、トップページの左上に青い「保存」ボタンが1つだけ表示された状態になります。背景は前節で適用したグローバルCSSの薄いグレー(#f9fafb)が引き続き効いていることも確認します。ボタンをクリックすると 保存しました のアラートが出ることを確認します。さらに、DevTools(F12 キー、または右クリック→「検証」)のElementsタブで <button>class 属性を確認すると、Button_button__xxxxx のように自動生成されたクラス名が付与されており、コンポーネント単位でクラス名がスコープ化されている様子を確認できます。

コードを解説します。

'use client';

Button.tsxapp/page.tsx の先頭に付けているディレクティブで、そのファイルをClient Componentとして扱うことをNext.jsに伝えます。<button>onClickのようにブラウザ側で動くイベントハンドラを含むコンポーネントや、そのハンドラを関数propとして渡す側のコンポーネントはClient Componentである必要があります。前章のNext.jsの基本の「Server ComponentとClient Componentの使い分け」で扱った内容です。

import styles from './Button.module.css';

.module.css ファイルをオブジェクトとして import しています。styles オブジェクトのプロパティが、CSSファイルで定義した各クラス名に対応します。

<button className={styles.button} onClick={onClick}>

styles.buttonclassName に渡すことで、ビルド時に自動生成される一意なクラス名(Button_button__xxxxx のような形式)が要素に付与されます。この仕組みにより、他ファイルで .button を定義してもクラス名が衝突しません。

5.2 クラス名の動的な切り替え

CSS Modulesでは、コンポーネントに渡された props の値に応じて、適用するクラス名を動的に切り替えることができます。たとえば同じButtonでも、普段は通常のボタンを表示しておき、削除や取り消しといったユーザに注意を促したい場面では赤系の「危険」を示すクラスに切り替える、といった使い分けが典型例です。

ここでは、Buttonコンポーネントに「通常(primary)」と「危険操作用(danger)」の2種類のバリエーションを持たせ、variant prop の値によって適用するクラスを切り替えられるようにします。

バリエーション用CSSクラスの追加

まずCSSファイル側にバリエーション用のクラスを追加します。app/components/Button.module.css を以下の内容に置き換えます。

.button {
  padding: 8px 16px;
  border: none;
  border-radius: 6px;
  color: #ffffff;
  font-size: 14px;
  cursor: pointer;
}

.primary {
  background-color: #2563eb;
}

.primary:hover {
  background-color: #1d4ed8;
}

.danger {
  background-color: #dc2626;
}

.danger:hover {
  background-color: #b91c1c;
}

tsxファイルでの動的なクラス名切り替え

コンポーネント側では、propsで受け取ったバリエーションに応じて、テンプレートリテラルで複数のクラス名を組み合わせます。書き方は以下のとおりです。

className={`${styles.共通クラス} ${styles[propsの変数名]}`}

以下は、variant propの値に応じて .primary または .danger を切り替える例です。app/components/Button.tsx を以下の内容に置き換えます。

'use client';

import styles from './Button.module.css';

type Variant = 'primary' | 'danger';

type Props = {
  label: string;
  variant?: Variant;
  onClick?: () => void;
};

export function Button({ label, variant = 'primary', onClick }: Props) {
  return (
    <button
      className={`${styles.button} ${styles[variant]}`}
      onClick={onClick}
    >
      {label}
    </button>
  );
}

画面への配置と動作確認

呼び出し側のapp/page.tsxを、チェックボックスで「危険な操作モード」を切り替えると、同じButtonの見た目・ラベル・アラート文言が動的に切り替わる形に置き換えます。

'use client';

import { useState } from 'react';
import { Button } from './components/Button';

export default function Home() {
  const [isDangerMode, setIsDangerMode] = useState(false);
  return (
    <main style={{ padding: '32px', display: 'flex', flexDirection: 'column', gap: '12px', alignItems: 'flex-start' }}>
      <label>
        <input
          type="checkbox"
          checked={isDangerMode}
          onChange={(e) => setIsDangerMode(e.target.checked)}
        />
        {' '}危険な操作モード
      </label>
      <Button
        label={isDangerMode ? '削除' : '保存'}
        variant={isDangerMode ? 'danger' : 'primary'}
        onClick={() => alert(isDangerMode ? '削除しました' : '保存しました')}
      />
    </main>
  );
}

保存すると、ブラウザで開いている http://localhost:3000 のトップページに、危険な操作モード のチェックボックスと、その下に1つのボタンが縦に並んで表示されます。チェックボックスがオフの初期状態では、以下のようにボタンは青い「保存」で、クリックすると 保存しました のアラートが出ます。

チェックボックスをオンにすると、同じ1つのButtonの色が赤に、ラベルが「削除」に切り替わり、クリック時のアラートも 削除しました に変わります。

DevTools(F12 キー、または右クリック→「検証」)のElementsタブで <button>class 属性を観察すると、チェックボックスの切り替えに応じて Button_button__xxxxx Button_primary__yyyyyButton_button__xxxxx Button_danger__zzzzz の間でクラス名が動的に入れ替わる様子を確認できます。

コードを解説します。

type Variant = 'primary' | 'danger';

variant prop が取りうる値を文字列リテラル型で定義しています。'primary''danger' 以外の値を渡すと、TypeScriptがコンパイル時にエラーとして検出します。

className={`${styles.button} ${styles[variant]}`}

テンプレートリテラルで2つのクラス名を半角スペース区切りで連結しています。前半の styles.button は常に適用される共通スタイルで、後半の styles[variant]variant の値に応じて .primary または .danger に切り替わります。styles[variant] のように角括弧でアクセスすると、propsの値によって取り出すクラスを動的に切り替えられます。

const [isDangerMode, setIsDangerMode] = useState(false);
...
variant={isDangerMode ? 'danger' : 'primary'}

useStateで管理している isDangerMode の値に応じて、variant propに渡す文字列を三項演算子で切り替えています。propの値が変わるたびにReactがButtonを再レンダリングし、Button内部の styles[variant] によって適用されるクラス名が .primary.danger の間で切り替わります。同じButtonコンポーネントのインスタンスに対して、状態に応じて適用するCSSクラスを動的に差し替えられるのがCSS Modulesの利点です。

CSSは通常のCSSとまったく同じ構文で書けるため、これまでのCSS知識をそのまま活かせる点もCSS Modulesの利点です。加えて、クラス名の衝突を気にせずコンポーネント単位でスタイルを閉じ込められます。

6. Tailwind CSSの考え方

CSS Modulesとは方向性の異なるアプローチとして、Tailwind CSSがあります。Tailwind CSSは「1つのプロパティを持つ小さなクラス(ユーティリティクラス)」を大量に用意しておき、それらを組み合わせてスタイリングするユーティリティファーストという設計思想のフレームワークです。

Tailwind CSS公式ドキュメント(Styling with utility classes)では、この考え方が次のように紹介されています。

You style things with Tailwind by combining many single-purpose presentational classes (utility classes) directly in your markup:

つまり、独自のクラス名を作って別ファイルにスタイルを書くのではなく、HTML(JSX)のクラス名に直接ユーティリティを並べるのがTailwindの基本的な書き方です。

Tailwind CSSはcreate-next-appのデフォルト構成(本章の冒頭でYes, use recommended defaultsを選んだ設定)に含まれるため、追加のセットアップなしで使えます。既存プロジェクトに後から追加する場合の手順はInstall Tailwind CSS with Next.js(Tailwind CSS公式ドキュメント)に記載があります。

6.1 ユーティリティクラスの例

Tailwindでは、CSSプロパティごとに短いクラス名が用意されています。よく使うものの一例は以下のとおりです。

クラス 対応するCSS 用途
p-4 padding: 1rem 全方向にpaddingを設定する
px-4 padding-left / right: 1rem 横方向にpaddingを設定する
mt-2 margin-top: 0.5rem 上のマージンを設定する
text-white color: #fff 文字色を指定する
bg-blue-600 background-color: #2563eb 背景色を指定する
rounded-md border-radius: 6px 角を丸くする
hover:bg-blue-700 :hover時のbackground ホバー時のスタイルを指定する

数字は「4→1rem」「2→0.5rem」のように、あらかじめ決められたスケールに沿っています。色もblue-600 blue-700のように段階が用意されており、この決まったパレットに沿って書くことでプロジェクト全体のデザインが自然に揃うようになっています。

6.2 同じButtonのTailwindでの書き直し

Tailwindの書き方を掴むため、先ほどCSS Modulesで作ったButtonコンポーネントを、Tailwindで書き直します。ユーティリティクラスをclassNameに直接並べる形式では、propsで受け取ったバリエーションに応じて、テンプレートリテラルでクラス文字列を切り替えます。書き方は以下のとおりです。

className={`共通ユーティリティ ${条件によって切り替えるユーティリティ}`}

以下は、variant propの値に応じて背景色を切り替えるTailwind版のButtonです。あわせて、Tailwindが得意とするshadow-md hover:shadow-lg)とアニメーションtransition-all duration-300)のユーティリティも追加し、CSS Modules版と見た目で区別できるようにします。app/components/Button.tsx を以下の内容に置き換えます。

'use client';

type Variant = 'primary' | 'danger';

type Props = {
  label: string;
  variant?: Variant;
  onClick?: () => void;
};

const variantClasses: Record<Variant, string> = {
  primary: 'bg-blue-600 hover:bg-blue-700',
  danger: 'bg-red-600 hover:bg-red-700',
};

export function Button({ label, variant = 'primary', onClick }: Props) {
  return (
    <button
      className={`px-4 py-2 rounded-md text-white text-sm cursor-pointer shadow-md hover:shadow-lg transition-all duration-300 ${variantClasses[variant]}`}
      onClick={onClick}
    >
      {label}
    </button>
  );
}

呼び出し側のapp/page.tsxも、Tailwind CSSに合わせてmainのレイアウトをTailwindのユーティリティクラスに書き換えます。CSS Modules版で使っていた React の inline style を Tailwind クラスに置き換えるだけで、Buttonへ渡すpropsは同じです。

'use client';

import { useState } from 'react';
import { Button } from './components/Button';

export default function Home() {
  const [isDangerMode, setIsDangerMode] = useState(false);
  return (
    <main className="p-8 flex flex-col gap-3 items-start">
      <label>
        <input
          type="checkbox"
          checked={isDangerMode}
          onChange={(e) => setIsDangerMode(e.target.checked)}
        />
        {' '}危険な操作モード
      </label>
      <Button
        label={isDangerMode ? '削除' : '保存'}
        variant={isDangerMode ? 'danger' : 'primary'}
        onClick={() => alert(isDangerMode ? '削除しました' : '保存しました')}
      />
    </main>
  );
}

保存すると、ブラウザで開いている http://localhost:3000 のトップページに、CSS Modules版と同じ配置でチェックボックスとボタンが表示されます。CSS Modules版と比べて、以下の順で見た目の違いを確認していきます。

チェックボックスがオフの初期状態では、青い「保存」ボタンが表示され、その下に shadow-md によるやや薄い影が付きます。

「保存」ボタンにマウスカーソルを乗せると、hover:shadow-lg の効果で影がふわっと大きく広がります。

チェックボックス「危険な操作モード」をオンにすると、variant の切り替えに応じてボタンの色とラベルが青い「保存」から赤い「削除」に切り替わります。transition-all duration-300 によって、色の変化は瞬時ではなく 0.3 秒かけてなめらかに補間されます。

赤い「削除」ボタンでも同じく、マウスカーソルを乗せると影が大きく広がります。

CSS Modules版でこれらを実現しようとすると、.buttonbox-shadowtransition のプロパティを追記する必要があります。Tailwindでは既存の className にユーティリティクラスを1つずつ足すだけで済むのが、ユーティリティファーストの強みです。

コードを解説します。

const variantClasses: Record<Variant, string> = {
  primary: 'bg-blue-600 hover:bg-blue-700',
  danger: 'bg-red-600 hover:bg-red-700',
};

variantの値ごとに対応するユーティリティクラスの文字列を、Record<Variant, string> 型のオブジェクトにまとめています。Recordはキーと値の型を指定した辞書型で、ここでは'primary' 'danger'をキー、対応するクラス文字列を値として持ちます。

className={`px-4 py-2 rounded-md text-white text-sm cursor-pointer shadow-md hover:shadow-lg transition-all duration-300 ${variantClasses[variant]}`}

テンプレートリテラルで、常に適用される共通ユーティリティと、variantに応じて切り替わる背景色ユーティリティを連結しています。今回追加した3つのユーティリティは以下の役割を持ちます。

  • shadow-md: 要素の下にほどよい大きさの影を落とす
  • hover:shadow-lg: マウスカーソルを乗せたとき(:hover)だけ、影をさらに大きい shadow-lg に切り替える
  • transition-all duration-300: このボタンで発生するすべてのプロパティ変化(背景色・影など)を、瞬時ではなく0.3秒(300ms)かけてなめらかに補間する

CSS Modulesではstyles.buttonのようにオブジェクトのプロパティ経由でクラス名を参照していたのに対し、Tailwindではユーティリティクラス名の文字列をそのまま並べる形になります。別ファイルを開かなくても、JSXのclassNameを見るだけで「どんな見た目・アニメーションが付いているか」がすべて把握できるのがTailwindの特徴です。一方で、classNameの中身が長くなりやすい傾向もあります。

Tailwindの動作確認ができたら、以降のハンズオンはCSS Modulesで進めるため、app/components/Button.tsxapp/page.tsx を前節「クラス名の動的な切り替え」で示したCSS Modules版のコードに戻して保存してください。

7. CSS ModulesとTailwind CSSの使い分け

CSS ModulesとTailwind CSSはどちらか一方が優れているわけではなく、プロジェクトの性質やチームの好みによって選ばれます。それぞれの向き・不向きを整理すると以下のようになります。

観点 CSS Modules Tailwind CSS
学習コスト 通常のCSS知識をそのまま活かせる クラス名の命名規則を新たに覚える必要がある
ファイル構成 .tsx.module.cssが対で並ぶ .tsx1ファイルで完結する
スタイルの局所性 ファイルが分かれる分、ロジックとスタイルを視線で切り替えられる JSXを見るだけでスタイルまで把握できる
デザインシステム 独自ルールをCSS変数などで自前で作り込む必要がある あらかじめ用意されたスケール(色・余白・フォントなど)に自然に揃う
動的な値の扱い メディアクエリや擬似クラスを含む複雑なCSSも自然に書ける 動的な計算値やインラインの数値はクラスで表現しづらい場面がある
相性の良いチーム CSSに慣れたチームや、既存のCSS資産があるプロジェクトで選ばれる 素早く一貫したUIを作りたいチームや、デザインシステムをフレームワークに任せたいプロジェクトで選ばれる

「どちらが正解」というものではないため、実際にはプロジェクトの方針に従うのが基本です。本講座では、CSS Modulesが標準的なCSS知識をそのまま活かせて学習の連続性が高いことから、以降の章でもCSS Modulesを用いてスタイリングを進めます。

💡 ポイント
CSS ModulesとTailwind CSSは同じプロジェクト内で併用することもできます。「レイアウトの余白などはTailwindのユーティリティで手早く決め、複雑な状態遷移が絡むコンポーネントだけCSS Modulesで丁寧に書く」といった使い分けもよく採用されます。

8. レスポンシブデザインの実現

HTML/CSSの基本で扱ったように、レスポンシブデザインとは、画面幅に応じてレイアウトや文字サイズを切り替える設計手法です。ここでは、CSS ModulesとTailwind CSSそれぞれで、どうレスポンシブを実現するかを確認します。

8.1 CSS Modulesでのメディアクエリ

CSS Modulesは中身が通常のCSSなので、メディアクエリもそのまま書けます。書き方は以下のとおりです。

セレクタ {
  /* デフォルト(最も狭い画面向け)のスタイル */
}

@media (min-width: ブレークポイント) {
  セレクタ {
    /* 指定した画面幅以上で上書きするスタイル */
  }
}

CSSファイルの作成

以下は、画面幅が768px以上のときにカードの横並びを2カラムに、1024px以上で3カラムに切り替える例です。Visual Studio Codeのエクスプローラーで app/components フォルダを右クリックし、「新しいファイル」から CardList.module.css を作成します。

styling-practice/
└── app/
    └── components/
        ├── Button.module.css
        ├── Button.tsx
        └── CardList.module.css  ← このファイルを作成

作成した CardList.module.css に以下の内容を書き込みます。

.list {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
  padding: 16px;
}

.card {
  padding: 16px;
  border-radius: 8px;
  background-color: #ffffff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

@media (min-width: 768px) {
  .list {
    grid-template-columns: 1fr 1fr;
  }
}

@media (min-width: 1024px) {
  .list {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

tsxファイルの作成

Visual Studio Codeのエクスプローラーで components フォルダを右クリックし、「新しいファイル」から CardList.tsx を作成します。

styling-practice/
└── app/
    └── components/
        ├── Button.module.css
        ├── Button.tsx
        ├── CardList.module.css
        └── CardList.tsx  ← このファイルを作成

作成した CardList.tsx に以下の内容を書き込みます。

import styles from './CardList.module.css';

export function CardList() {
  const items = ['カード1', 'カード2', 'カード3', 'カード4', 'カード5', 'カード6'];
  return (
    <ul className={styles.list}>
      {items.map((item) => (
        <li key={item} className={styles.card}>{item}</li>
      ))}
    </ul>
  );
}

画面への配置と動作確認

app/page.tsx を、CardList コンポーネントを表示する形に置き換えます。

import { CardList } from './components/CardList';

export default function Home() {
  return (
    <main>
      <CardList />
    </main>
  );
}

保存すると、ブラウザで開いている http://localhost:3000 のトップページに6枚のカードが並びます。ブラウザのウィンドウ幅を段階的に狭めていくと、以下のように 3列→2列→1列 とレイアウトが切り替わることを確認できます。DevTools(F12 キー、または右クリック→「検証」)のデバイスモード(Toggle Device Toolbar)で iPhone や iPad の画面幅に切り替えて確認するのも便利です。

まず、幅1024px以上の広い画面では、@media (min-width: 1024px) の指定が効いて3列レイアウトで表示されます。

ウィンドウを狭めて幅768px〜1023pxの範囲に入ると、@media (min-width: 768px) の指定が効いて2列レイアウトに切り替わります。

さらに狭めて幅768px未満になると、どちらのメディアクエリにも当てはまらなくなり、デフォルトの grid-template-columns: 1fr が適用されて1列レイアウトに切り替わります。

コードを解説します。

.list {
  display: grid;
  grid-template-columns: 1fr;
  gap: 16px;
  padding: 16px;
}

@mediaの外側でデフォルトのスタイルを定義しています。grid-template-columns: 1frで1カラム表示を指定しており、これはメディアクエリに一致しない狭い画面(スマートフォン)向けのスタイルとして扱われます。

@media (min-width: 768px) {
  .list {
    grid-template-columns: 1fr 1fr;
  }
}

@media (min-width: 1024px) {
  .list {
    grid-template-columns: 1fr 1fr 1fr;
  }
}

画面幅が768px以上のときに2カラム、1024px以上のときに3カラムへと上書きしています。狭い画面を起点にして、画面幅が広がるたびにスタイルを上書きしていくこの考え方をモバイルファーストと呼びます。

8.2 Tailwind CSSのブレークポイントプレフィックス

Tailwind CSSでは、メディアクエリを書く代わりにブレークポイントプレフィックスsm: md: lg: xl:)をクラス名の先頭に付けて、画面幅ごとの適用を表現します。書き方は以下のとおりです。

<要素タグ className="デフォルトのユーティリティクラス ブレークポイント:上書きするユーティリティクラス">

以下は、モバイル(デフォルト)では1カラム、md:(768px以上)で2カラム、lg:(1024px以上)で3カラムに切り替える例です。app/components/CardList.tsx を以下の内容に置き換えます。

export function CardList() {
  const items = ['カード1', 'カード2', 'カード3', 'カード4', 'カード5', 'カード6'];
  return (
    <ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
      {items.map((item) => (
        <li key={item} className="p-4 rounded-lg bg-white shadow">{item}</li>
      ))}
    </ul>
  );
}

保存すると、前節「CSS Modulesでのメディアクエリ」と同様、ウィンドウ幅を狭めるとカードのレイアウトが 3列→2列→1列 と段階的に切り替わります。

コードを解説します。

<ul className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">

プレフィックスなしの grid-cols-1 はモバイル向けのデフォルトとして常に適用されます。md:grid-cols-2min-width: 768px 以上、lg:grid-cols-3min-width: 1024px 以上で有効になります。Tailwindのブレークポイントもモバイルファーストの考え方に基づいており、「小さい画面のスタイルをデフォルトにして、大きい画面向けに上書きする」という点はCSS Modulesと共通です。

📝 モバイルファーストが標準になっている理由
スマートフォンからのアクセスが主流となった現在では、モバイル向けのレイアウトを土台にしてから、画面幅の広いPC向けの装飾を追加するのが基本です。逆にPC向けを土台にすると、モバイル用に多くのプロパティを打ち消す必要が生じ、CSSが煩雑になりがちです。

9. まとめ

この章では、Next.jsでのスタイリングを学びつつ、実際にコードで動作を確認しました。

  • Next.jsでは、グローバルCSS・CSS Modules・Tailwind CSSなど複数のスタイリング手法が使える
  • グローバルCSSは app/globals.css に書き、app/layout.tsx で import することで全ページに適用される
  • CSS Modulesは xxx.module.css という命名規約でクラス名が自動的にスコープ化され、コンポーネント間の衝突を防げる
  • CSS Modulesでは import styles from './Xxx.module.css' し、className={styles.xxx} の形で参照する
  • 複数のクラス名を組み合わせたいときは、テンプレートリテラルで ${styles.a} ${styles.b} のように連結する
  • Tailwind CSSは、事前定義のユーティリティクラスをHTMLに直接書くユーティリティファーストの思想で、JSX1ファイルでスタイル・構造・振る舞いを把握できる
  • CSS ModulesとTailwind CSSに優劣はなく、プロジェクトの方針・チームの習熟度・デザインシステムの有無で選ぶ(本講座では以降CSS Modulesで進める)
  • レスポンシブデザインは、CSS Modulesではメディアクエリ、Tailwindでは sm: md: lg: などのブレークポイントプレフィックスで実現でき、いずれもモバイルファーストで実装する

次の章では、Route Handlersと fetch API、TanStack Queryを使ったフロントエンドからバックエンドへのデータ取得を学びつつ、実際にコードで動作を確認します。

この教材は役に立ちましたか?

いいねをたくさんいただけると、制作者の励みになり、より多くのセクションが作れるようになります。

感想を一言(任意)

いただいたコメントは次の制作のヒントになります。ぜひお気軽にご投稿ください。

このコメントは他の受講生には公開されません。DevOps Camp運営が、教材改善のために確認します。

0 / 2000