
Tailwind CSSでレスポンシブ・ダークモードを実践する基本パターン
Tailwind CSSでレスポンシブ対応とダークモードを実装する基本パターンを解説します。画面幅ごとのレイアウト調整、dark:の使い方、初級者がつまずきやすいポイントを整理します。
シリーズ:Tailwind CSS v4 入門
1
v3→v4 変更点まとめ
2
TS + Next.js / Bun セットアップ
3
@theme でデザイントークン管理
4
レスポンシブ・ダークモード実践
5
動的クラスと @source inline()
6
コンポーネント設計パターン集
第3回でプロジェクト全体のデザイントークンを整備しました。今回はそのトークンを土台にして「どの画面サイズでも崩れないレイアウト」と「ダークモード対応」を実装します。
どちらも Tailwind CSS を学ぶ上で必ずぶつかる壁ですが、 仕組みさえ理解すれば書き方は非常にシンプル です。まずレスポンシブから順番に掘り下げていきます。
🎯 この記事のゴール
レスポンシブクラスの書き方(モバイルファーストの考え方)を理解し、実際のカードレイアウトをレスポンシブ対応できる状態にします。また v4 でのダークモード対応の3つの方法と、それぞれの使い分けを把握します。
PART 1 ── レスポンシブデザイン
ブレークポイントとは何か
ブレークポイントとは「この画面幅を超えたらレイアウトを変える」という境界線のことです。Tailwind には最初から5つのブレークポイントが用意されています。
デフォルトのブレークポイント(小さい順)
(なし)0px〜
sm: 640px〜
md: 768px〜
lg: 1024px〜
xl: 1280px〜
2xl: 1536px〜
⚠️ バーの長さは画面幅の比率を表すイメージ図です
プレフィックスなしのクラス(例:text-sm )はすべての画面サイズに適用されます。md:text-lg と書くと「768px 以上の画面でのみtext-lg を適用する」という意味になります。
💡 モバイルファーストという考え方
Tailwind のブレークポイントは「 この幅以上のとき 」を意味します(min-width ベース)。つまり プレフィックスなしのスタイルがモバイル向けのベースになり、プレフィックスを付けてより大きな画面向けに上書き していきます。これを「モバイルファースト」と呼びます。
「sm: より小さいとき」を表す書き方はデフォルトでは存在しません。プレフィックスなしをモバイル向けに書いておき、大きくなるにつれて追記するのが Tailwind の作法です。
レスポンシブクラスの基本の書き方
具体的なコードで確認しましょう。テキストサイズをデバイスに応じて変える例です。
// モバイル:text-base / タブレット:text-lg / PC:text-2xl
<h1 className="text-base md:text-lg lg:text-2xl">
見出しテキスト
</h1>
// モバイル:縦並び / タブレット以上:横並び
<div className="flex flex-col md:flex-row gap-4">
<div>左カラム</div>
<div>右カラム</div>
</div>
// モバイル:1列 / タブレット:2列 / PC:3列グリッド
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div>カード1</div>
<div>カード2</div>
<div>カード3</div>
</div>クラス名に色をつけると対応関係がわかりやすくなります。
(なし) モバイル(全幅)
sm : 640px〜
md : 768px〜
lg : 1024px〜
md : ダークモード
grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6p-4 md:p-8 text-sm md:text-base lg:text-lg
レスポンシブを体感する:ライブデモ
ボタンを押して画面幅を切り替えると、同じコードがどのように見え方を変えるかを確認できます。
MyApp
☰
Tailwind CSS v4 で
レスポンシブを体感
画面幅に応じてレイアウトが変わります
詳しく見る
カード A
grid-cols-1 → 2 → 3
カード B
md:grid-cols-2 が効いています
カード C
lg:grid-cols-3 が効いています
📱 モバイル(〜767px): grid-cols-1
よく使うレスポンシブパターン集
パターン①:ナビゲーションのハンバーガーメニュー対応
<nav className="flex items-center justify-between px-4 py-3">
<span>ロゴ</span>
{/* PC のみ表示するリンク群 */}
<ul className="hidden md:flex gap-6">
<li>ホーム</li>
<li>サービス</li>
</ul>
{/* モバイルのみ表示するハンバーガーアイコン */}
<button className="md:hidden">☰</button>
</nav>✨ hidden と block の使い分け
hidden はdisplay: none を意味します。md:flex は「768px以上でdisplay: flex にする」です。hidden md:flex と組み合わせると「モバイルでは非表示、タブレット以上で flex 表示」になります。逆に「モバイルのみ表示」させたい場合はblock md:hidden と書きます。
パターン②:サイドバーレイアウト
<div className="flex flex-col lg:flex-row gap-8">
{/* サイドバー:モバイルは全幅、PC は固定幅 */}
<aside className="w-full lg:w-64 shrink-0">
サイドバーコンテンツ
</aside>
{/* メインコンテンツ:残りの幅をすべて使う */}
<main className="flex-1 min-w-0">
メインコンテンツ
</main>
</div> パターン③:画像とテキストの切り替え
<section className="flex flex-col md:flex-row-reverse items-center gap-8 py-12">
<img
className="w-full md:w-1/2 rounded-xl"
src="..." alt="..."
/>
<div className="md:w-1/2 space-y-4">
<h2 className="text-2xl md:text-3xl font-bold">見出し</h2>
<p className="text-gray-600 leading-relaxed">説明文</p>
</div>
</section> カスタムブレークポイントを追加する
デフォルトの5つで足りない場合は、第3回で学んだ@theme を使ってブレークポイントを追加できます。
@theme {
/* カスタムブレークポイント:定義するとプレフィックスとして使える */
--breakpoint-xs: 480px; /* → xs:クラス名 で使用可能 */
--breakpoint-3xl: 1920px; /* → 3xl:クラス名 で使用可能 */
}// xs: は 480px 以上で適用される
<div className="text-sm xs:text-base md:text-lg">
極小デバイスから PC まで細かく制御できる
</div>⚠️ ブレークポイントは増やしすぎない
ブレークポイントが増えるとクラスの組み合わせが爆発的に複雑になります。まずデフォルトの5つで対応できないか検討し、本当に必要な場合だけ追加するのがベストプラクティスです。多くの場合はsm: ・md: ・lg: の3つで十分です。
PART 2 ── ダークモード
ダークモード対応の3つの戦略
Tailwind v4 でダークモードを実装する方法は大きく3つあります。プロジェクトの要件に合わせて選んでください。
STRATEGY 01
OS設定に自動追従
prefers-color-scheme メディアクエリを使い、OSのライト/ダーク設定に自動で合わせる。設定コードが最小で済む。
✓ v4 デフォルト
STRATEGY 02
クラスで手動切り替え
<html> タグにclass="dark" を付け外しして切り替える。ボタンでテーマを切り替えるUIを作れる。
要 @custom-variant
STRATEGY 03
OS設定 + 手動切り替え
OS設定をデフォルトとしつつ、ユーザーが手動でも切り替えられる。最も実用的だが実装が少し複雑になる。
推奨・実装は後述
Strategy 01:OS設定に自動追従する(最もシンプル)
v4 のデフォルト動作です。追加の設定は何もいりません。dark: プレフィックスを付けたクラスが、OS がダークモードのときに自動で適用されます。
// ライト:白背景・黒文字 / ダーク:濃紺背景・白文字
<div className="bg-white text-gray-900 dark:bg-slate-900 dark:text-slate-50">
コンテンツ
</div>
// カードコンポーネントの例
<div className="
rounded-xl border p-6
bg-white border-gray-200 text-gray-900
dark:bg-slate-800 dark:border-slate-700 dark:text-slate-100
">
<h3 className="font-bold text-lg mb-2 dark:text-white">
カードタイトル
</h3>
<p className="text-gray-600 dark:text-slate-400">
説明テキスト
</p>
</div>💡 v3 との違い:設定なしで動く
v3 ではtailwind.config.js にdarkMode: 'media' と書く必要がありました。v4 ではこの設定が完全に不要になり、dark: プレフィックスが最初から使えます。
クラスの対応関係をカラーで確認しましょう。
(なし) モバイル(全幅)
sm : 640px〜
md : 768px〜
lg : 1024px〜
md : ダークモード
bg-white dark:bg-slate-900 text-gray-900 dark:text-slate-100 border-gray-200 dark:border-slate-700
ダークモードを体感する:ライブデモ
タブを押してライト/ダークを切り替えて、スタイルの変化を確認してください。
Tailwind CSS v4
dark: プレフィックスの動作確認
ライトモードでは白背景・濃い文字色。ダークモードに切り替えると背景が暗くなり、文字色が明るくなります。
v4 新機能
最終更新:2026年
Strategy 02:クラスで手動切り替えする
「ボタンでダークモードを切り替えたい」場合は、@custom-variant を使ってdark: の判定ロジックを変更します。
ステップ①:CSS で dark バリアントを再定義する
// V3 : tailwind.config.js で設定していた
darkMode: 'class', // JS 設定で切り替え方法を変えていた/* globals.css / index.css等 に追記する */
@import "tailwindcss";
/*
* dark: プレフィックスの判定を
* 「OS設定」から「.dark クラスの有無」に切り替える
*/
@custom-variant dark (&:where(.dark, .dark *)); ステップ②:HTML 要素に class="dark" を付け外しする
"use client";
import { useState, useEffect } from "react";
export function ThemeToggle() {
const [dark, setDark] = useState(false);
useEffect(() => {
// <html> タグに "dark" クラスを付け外しする
document.documentElement.classList.toggle("dark", dark);
}, [dark]);
return (
<button
onClick={() => setDark(d => !d)}
className="rounded-full p-2 bg-gray-100 dark:bg-slate-700 transition-colors"
>
{dark ? "☀ ライト" : "🌙 ダーク"}
</button>
);
} Strategy 03:OS設定に追従しつつ手動でも切り替える(推奨)
実際のプロダクトでは「OS がダークなら最初はダークモードで起動し、ユーザーが手動でも変更できる」という仕様が最も親切です。localStorage を使って設定を記憶します。
/* globals.css / index.css(CSS 側の設定) */
@import "tailwindcss";
/*
* Strategy 03 用:
* .dark クラスが付いていれば dark: を適用。
* OS設定との併用は JS 側で制御する(後述)
*/
@custom-variant dark (&:where(.dark, .dark *));// useTheme.ts(初期化 + 切り替えロジック)
"use client";
import { useState, useEffect } from "react";
export function useTheme() {
const [theme, setTheme] = useState<"light" | "dark" | "system">("system");
useEffect(() => {
// 1. localStorage に保存済みの設定を読む
const saved = localStorage.getItem("theme") as typeof theme | null;
// 2. 設定がなければ OS 設定を参照する
const initial = saved ?? "system";
setTheme(initial);
applyTheme(initial);
}, []);
const applyTheme = (t: typeof theme) => {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const isDark =
t === "dark" || (t === "system" && prefersDark);
// <html> タグに .dark を付け外し
document.documentElement.classList.toggle("dark", isDark);
};
const changeTheme = (t: typeof theme) => {
setTheme(t);
localStorage.setItem("theme", t);
applyTheme(t);
};
return { theme, changeTheme };
}// ThemeSelector.tsx(ライト / ダーク / システム の3択UI)
import { useTheme } from "./useTheme";
export function ThemeSelector() {
const { theme, changeTheme } = useTheme();
return (
<div className="flex gap-2 rounded-lg bg-gray-100 dark:bg-slate-800 p-1">
{(["light", "system", "dark"] as const).map((t) => (
<button
key={t}
onClick={() => changeTheme(t)}
className={`px-3 py-1 rounded-md text-sm font-medium transition-colors
${theme === t
? "bg-white dark:bg-slate-600 shadow text-gray-900 dark:text-white"
: "text-gray-500 dark:text-slate-400 hover:text-gray-700"
}`}
>
{ t === "light" ? "☀ ライト" : t === "dark" ? "🌙 ダーク" : "🖥 システム" }
</button>
))}
</div>
);
} @theme のトークンをダークモードと組み合わせる
第3回で定義したデザイントークンをダークモードに対応させる方法も確認しておきましょう。@theme で定義した CSS 変数はカスケードで上書きできるため、ダークモード用の値を簡潔に書けます。
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
@theme {
/* ライト時のデザイントークン */
--color-surface: #ffffff;
--color-surface-2: #f5f5f3;
--color-on-surface: #1a1a1a;
--color-on-muted: #555555;
--color-brand-500: #0ea5e9;
}
/* ダーク時:.dark クラスが html に付いていると上書きされる */
.dark {
--color-surface: #0f172a;
--color-surface-2: #1e293b;
--color-on-surface: #f1f5f9;
--color-on-muted: #94a3b8;
--color-brand-500: #38bdf8; /* 少し明るめに調整 */
} こうすると、コンポーネント側にはdark: プレフィックスをほとんど書かなくて済みます。
// --color-surface が .dark クラスで自動上書きされるため
// dark: プレフィックスが不要になる
<div className="bg-surface text-on-surface border border-surface-2">
{/* ブランドカラーも .dark で自動切り替え */}
<button className="bg-brand-500 text-white hover:opacity-90">
送信
</button>
</div>
// dark: が必要になるのは「ライトとダークで完全に別のクラスを使いたい」ときだけ
<img className="block dark:hidden" src="logo-light.svg" alt="ロゴ" />
<img className="hidden dark:block" src="logo-dark.svg" alt="ロゴ" />💡 セマンティックトークンとダークモードの組み合わせ
bg-white dark:bg-slate-900 のように「ライトの色・ダークの色」を毎回書くスタイルを「プリミティブトークン方式」と呼びます。対して--color-surface のように意味で命名して.dark ブロックで上書きする方法を「セマンティックトークン方式」と呼びます。コンポーネントが増えてくると後者の方がメンテナンスがはるかに楽になります。
よくあるトラブルと解決策
❓ dark: クラスを書いたのにダークモードが反映されない
Strategy 02・03 を使っている場合、<html> タグにclass="dark" が付いているか確認してください。ブラウザの DevTools で<html> 要素を確認するのが最も確実です。Strategy 01(OS自動追従)を使っている場合は@custom-variant dark の記述が CSS にないか確認してください(記述があると OS 設定ではなくクラス判定になります)。
❓ ページロード時に一瞬ライトモードが見えてからダークになる(フラッシュ)
JavaScript が読み込まれる前に CSS が適用されるため発生します。Next.js の場合はlayout.tsx の<html> タグより前に、インラインの<script> でlocalStorage を読んでクラスを付与するのが定石です。suppressHydrationWarning を<html> に付けることも忘れずに。
❓ レスポンシブクラスが思った通りに効かない
最も多い原因は「モバイルファーストの逆転」です。md:hidden は「768px 以上で非表示」を意味します。「768px 未満で非表示」にしたい場合はhidden md:block (デフォルトで非表示、768px 以上で表示)と書く必要があります。プレフィックスは「その幅以上に適用」という方向性を常に意識してください。
❓ sm: と md: どちらを使えばいいかわからない
目安として「スマートフォンとタブレットの境界」にsm: (640px)、「タブレットと PC の境界」にmd: (768px)を使うとシンプルです。レイアウトの大きな切り替え(1カラム → 2カラムなど)はmd: かlg: が適しています。sm: は余白や文字サイズの微調整に使うことが多いです。
第4回のまとめ
今回学んだこと
- Tailwind のブレークポイントは
min-widthベース。プレフィックスなし=モバイル向けのベースという「モバイルファースト」の考え方が基本 hidden md:flexでモバイル非表示・PC表示、block md:hiddenでモバイル表示・PC非表示が作れる- カスタムブレークポイントは
@themeに--breakpoint-名前: 値px;を書くだけで追加できる - v4 では
dark:プレフィックスが設定なしで最初から使えるようになった(v3 のdarkMode: 'media'設定が不要に) - クラス切り替え式(Strategy 02/03)は
@custom-variant dark (&:where(.dark, .dark *));を CSS に追記する @themeのセマンティックトークンを.dark { }ブロックで上書きする方法を使うと、コンポーネントへのdark:プレフィックス記述を最小限にできる
📌 第4回 まとめ
レスポンシブとダークモードの基本から、実用的なテーマ切り替えパターンまで一通り押さえました。次回は実際の開発でよく詰まる「動的に生成したクラス名がスタイルに反映されない」という問題を掘り下げます。v4 の@source inline() という新しい解決策を中心に解説します。
📝 ▶ 次回(第5回)
次は「 」です。
