Tailwind ダークモード:class と data-theme の比較
深夜3時、画面上で点滅する dark:bg-gray-900 を見つめて、初めて真剣に考え始めた。Tailwind のダークモード、class と data-theme どっちを使うべき?
正直、この2つの戦略でずっと悩んでた。ドキュメントを検索しても断片的な説明しか見つからない、組み合わせてもどうもピンと来ない。だから公式ドキュメント、GitHub の議論、人気コンポーネントライブラリのソースコードまで全部掘り下げて、最後に整理できた。この記事では私が踏んだ穴と、考えた判断を全部書き出して、一緒に話そう。
Tailwind ダークモードの3つの戦略
まず明確にしたい:Tailwind はデフォルトで3つのダークモード戦略を提供している、2つだけじゃない。
Media 戦略:システム自動追従
Media 戦略は Tailwind のデフォルト設定——正直、多くの人はデフォルトだと知らないかもしれない。prefers-color-scheme CSS メディアクエリを使って、ユーザーのシステムダークモード設定を自動検出。
<!-- 設定不要、システム設定に自動追従 -->
<div class="bg-white dark:bg-gray-900">
コンテンツはシステム設定に基づいて自動切り替え
</div>
メリットは明確:ゼロ設定、ユーザーは何もしなくて好みの表示が得られる。でもデメリットも痛い——ユーザーが自分で選べない。ライト環境でダークモードを使いたい人にとって、体験が不足。
Class 戦略:手動切り替え制御
Class 戦略は親要素(通常 <html>)に .dark クラスを追加してダークモードをトリガー。開発者が完全な制御権を持てる、ユーザーの手動切り替えと設定の永続化が可能。
<!-- JavaScript でクラス名を制御 -->
<html class="dark">
<body class="bg-white dark:bg-gray-900">
ダークモード有効
</body>
</html>
今最も広く使われているアプローチ。コミュニティドキュメントが豊富、各種サードパーティライブラリとの統合もスムーズ。
Data-theme 戦略:セマンティック属性セレクタ
Data-theme 戦略は data-theme="dark" 属性を使う、クラス名じゃない。セマンティクスがより明確、天然でマルチテーマ拡張をサポート。
<html data-theme="dark">
<body class="bg-white dark:bg-gray-900">
ダークモード有効
</body>
</html>
より多くのテーマへの拡張が特に簡単——data-theme="oled" または data-theme="sepia"、好きなように定義できる。複数の表示モードをサポートする必要がある場合、本当に使いやすい。
Class 戦略の詳細
実装原理
Class 戦略の核心原理は実はとてもシンプル:DOM ツリーの祖先要素に .dark クラスが存在すると、全ての dark:* 修饰符のスタイルが有効になる。
Tailwind v3 では、設定ファイルで有効化:
// tailwind.config.js
module.exports = {
darkMode: 'class',
// ...
}
生成される CSS セレクタ構造:
.dark .dark:bg-gray-900 {
background-color: #111827;
}
Tailwind v4 は新しい CSS-first 設定アプローチに変更、@custom-variant ディレクティブを使用:
/* global.css */
@import 'tailwindcss';
@custom-variant dark (&:where(.dark, .dark *));
注目すべきは :where() 擬似クラス——specificity をゼロまで下げる、他のスタイルの優先順位計算に干渉しない。この詳細はかなり重要。
JavaScript 切り替えロジック
ユーザー切り替えの実装、小さな JavaScript で十分:
// 現在のテーマを取得
function getTheme() {
return localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
}
// テーマを設定
function setTheme(theme) {
localStorage.setItem('theme', theme);
document.documentElement.classList.toggle('dark', theme === 'dark');
}
// 初期化
setTheme(getTheme());
このコードは3つを実行:localStorage からユーザー設定を読み取る、設定がない時はシステム追従、テーマ切り替えと保存。大部分のケースで十分。
白画面フラッシュ防止
ページ読み込み時の短い白画面フラッシュ——私もこの穴を踏んだ。理由は単純:JavaScript 実行前に、HTML はデフォルトのライトモードでレンダリング済み。
解決策は <head> に同期実行スクリプトを配置、DOM レンダリング前にテーマを設定:
<head>
<script>
// 同期実行、フラッシュ防止
if (localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
</script>
</head>
このスクリプトは同期必須——defer や async は使えない。
メリット・デメリット
メリット:
- 実装がシンプルで直感的、早く習得できる
- コミュニティリソースが豊富、各種フレームワークに成熟したソリューション
- next-themes 等のツールライブラリと良好に連携
- specificity が少し高く、スタイルオーバーライドが保証
デメリット:
.darkクラス名のセマンティクスが不明確——コードを見てダークモードだと理解するまで考える必要- マルチテーマ拡張には複数のクラス名が必要、管理が少し乱れる
- CSS 変数アプローチとの組み合わせには追加の適応が必要
Data-theme 戦略の詳細
実装原理
Data-theme 戦略の核心は属性セレクタを使用、クラスセレクタじゃない。Tailwind v4 では以下のように設定:
@import 'tailwindcss';
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
生成される CSS セレクタ:
[data-theme='dark'] .dark:bg-gray-900 {
background-color: #111827;
}
Tailwind v3 もサポート、配列設定が必要:
// tailwind.config.js
module.exports = {
darkMode: ['selector', '[data-theme="dark"]'],
}
CSS 変数との組み合わせ
正直、data-theme 戦略と CSS 変数アプローチは天生のペア。異なる data-theme 下で異なる変数値を定義できる:
/* globals.css */
:root {
--background: 0 0% 100%;
--foreground: 222 84% 5%;
}
[data-theme='dark'] {
--background: 222 84% 5%;
--foreground: 210 40% 98%;
}
[data-theme='oled'] {
--background: 0 0% 0%; /* 真黒 */
--foreground: 0 0% 100%;
}
Tailwind 設定でこれらの変数を参照:
// tailwind.config.js
module.exports = {
theme: {
extend: {
colors: {
background: 'hsl(var(--background))',
foreground: 'hsl(var(--foreground))',
}
}
}
}
こうすると、data-theme 属性を切り替えるだけで、これらの変数を使用する全スタイルが自動切り替え——各コンポーネントに dark: 修饰符を書く必要がない。この体験は本当に快適。
shadcn/ui の実践経験
shadcn/ui コンポーネントライブラリはデフォルトで data-theme + CSS 変数アプローチ。スタイルファイルをめくると、大量の定義が見える:
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
/* ... 更多变量 */
}
.dark,
[data-theme='dark'] {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
/* ... 更多变量 */
}
}
興味深いのは、.dark クラスと [data-theme='dark'] 属性を同時にサポート——異なるユーザーの習慣に対応。shadcn/ui を使う場合、どちらの方法でダークモードをトリガーしてもOK。
マルチテーマ拡張能力
Data-theme アプローチの最大のメリットはここ——マルチテーマサポート。OLED モード、読書モードの定義がシンプル:
<html data-theme="oled">
<!-- 真黒背景、OLED スクリーンに適合 -->
</html>
<html data-theme="sepia">
<!-- 淡黄色背景、読書に適合 -->
</html>
切り替えロジックは属性値を変更だけ:
function setTheme(theme) {
localStorage.setItem('theme', theme);
document.documentElement.dataset.theme = theme;
}
このような柔軟性、class 戦略では本当に容易に実現できない。
メリット・デメリット
メリット:
- セマンティクスが明確——
data-theme="dark"一目でダークモードとわかる - 天然でマルチテーマ拡張をサポート
- CSS 変数アプローチと非常にスムーズに統合
- shadcn/ui、daisyUI 等のライブラリがデフォルトでコンパチブル
デメリット:
- Tailwind v3 ではカスタムセレクタ設定が必要
-一部のサードパーティライブラリは適応が必要 - コミュニティドキュメントが相対的に少ない——でも改善中
2つの戦略比較マトリックス
比較表を整理、主要な维度を全て列出:
| 比較维度 | Class 戦略 | Data-theme 戦略 |
|---|---|---|
| 実装複雑度 | 低、設定シンプル | 中、属性セレクタ理解必要 |
| セマンティクス明確度 | 中、.dark の意味は考える必要 | 高、data-theme 直感 |
| マルチテーマ拡張 | 困難、複数クラス名必要 | 容易、属性値変更のみ |
| コミュニティサポート | 高、ドキュメント豊富 | 中、普及中 |
| CSS 変数統合 | 追加適応必要 | 天然フレンドリー |
| Tailwind v3 | darkMode: 'class' | darkMode: ['selector', '...'] |
| Tailwind v4 | @custom-variant | @custom-variant |
| サードパーティライブラリ互換 | 互換性チェック必要 | shadcn/ui 等天然コンパチブル |
| Specificity | 少し高い(クラスセレクタ) | 同じ(属性セレクタ) |
Class 戦略を選択する場面?
- シンプルプロジェクト、ライト/ダーク2つのモードだけ必要
- Next.js + next-themes の組み合わせ使用
- Tailwind v3 設定にチームが習熟
- 大量のコミュニティ事例を参照必要
Data-theme 戦略を選択する場面?
- 複数テーマをサポート必要(OLED、読書等)
- shadcn/ui 等のコンポーネントライブラリ使用
- CSS 変数アプローチと深く統合したい
- プロジェクトのセマンティクス要求が高い
フレームワーク統合実践
Astro 統合アプローチ
Astro と Tailwind の統合はシンプル、でも1つの穴がある——View Transitions の処理。
基本設定:
// astro.config.mjs
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
export default defineConfig({
vite: {
plugins: [tailwindcss()]
}
});
ダークモードスクリプト:
<!-- BaseLayout.astro の head に配置 -->
<script is:inline>
// フラッシュ防止の同期スクリプト
const theme = localStorage.getItem('theme') ||
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
// 或は data-theme 使用
// document.documentElement.dataset.theme = 'dark';
}
</script>
View Transitions 处理:
Astro の View Transitions はページ切り替え時に DOM を再レンダリング、ダークモード状態が容易に失われる。astro:after-swap イベントを監視してテーマを再設定:
<script>
document.addEventListener('astro:after-swap', () => {
const theme = localStorage.getItem('theme');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
});
</script>
このステップはかなり重要——多くの開発者が容易に漏れる、私もこの穴を踏んだ。
Next.js + next-themes 統合
Next.js プロジェクトは next-themes ライブラリの使用を推奨。テーマ切り替えの全ロジックをパッケージ、SSR 互換と hydration 処理も心配不要。
インストール:
npm install next-themes
Provider 設定:
// components/ThemeProvider.tsx
import { ThemeProvider } from 'next-themes';
export function ThemeProvider({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="class" // class 戦略使用
defaultTheme="system" // デフォルトシステム追従
enableSystem={true} // システム検出有効
disableTransitionOnChange // 切り替え時フラッシュ防止
>
{children}
</ThemeProvider>
);
}
data-theme 戦略に切り替え?attribute プロパティを変更だけ:
<ThemeProvider attribute="data-theme" defaultTheme="system">
Layout で使用:
// app/layout.tsx
import { ThemeProvider } from './components/ThemeProvider';
export default function RootLayout({ children }) {
return (
<html lang="ja">
<body>
<ThemeProvider>
{children}
</ThemeProvider>
</body>
</html>
);
}
切り替えボタンコンポーネント:
// components/ThemeToggle.tsx
import { useTheme } from 'next-themes';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
className="p-2 rounded-lg"
>
{theme === 'dark' ? '☀️' : '🌙'}
</button>
);
}
next-themes は自動的に localStorage 永続化、システム設定検出、hydration 問題を処理。安心。
Tailwind v4 新機能
Tailwind v4 は新しい CSS-first 設定アプローチを導入、ダークモード設定も変更。
@custom-variant ディレクティブ
以前 JavaScript 設定ファイルで定義した variant は、今 CSS で直接宣言:
@import 'tailwindcss';
/* Class 戦略 */
@custom-variant dark (&:where(.dark, .dark *));
/* Data-theme 戦略 */
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
メリットはより直感的——設定変更に JavaScript 再ビルド不要。
@theme ディレクティブで変数定義
data-theme 戦略と組み合わせて、@theme ディレクティブでテーマ変数を定義:
@import 'tailwindcss';
@custom-variant dark (&:where([data-theme='dark'], [data-theme='dark'] *));
@theme {
--color-primary: oklch(0.65 0.2 150);
--color-muted: oklch(0.9 0.02 200);
}
/* ダークモード下の変数オーバーライド */
[data-theme='dark'] {
--color-primary: oklch(0.7 0.15 180);
--color-muted: oklch(0.3 0.02 200);
}
そして直接これらの色を使用:
<button class="bg-primary text-white">ボタン</button>
data-theme 切り替え後、色は自動変化——dark:bg-primary-dark 這样的冗長スタイルを書く必要なし。
3状態切り替え実装
light/dark/system 3状態切り替え、window.matchMedia API と組み合わせ必要:
function setTheme(theme) {
if (theme === 'system') {
localStorage.removeItem('theme');
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.dataset.theme = isDark ? 'dark' : 'light';
} else {
localStorage.setItem('theme', theme);
document.documentElement.dataset.theme = theme;
}
}
// システム設定変更を監視
window.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', (e) => {
if (!localStorage.getItem('theme')) {
document.documentElement.dataset.theme = e.matches ? 'dark' : 'light';
}
});
こうすると、ユーザーは固定テーマを選択、或は常にシステム追従が可能。
ベストプラクティスまとめ
推奨戦略選択
大部分のプロジェクトに対して、私のアドバイスは:
- シンプルプロジェクト:class 戦略使用、シンプルな切り替えスクリプトで十分
- shadcn/ui 使用:直接 data-theme + CSS 変数アプローチ
- マルチテーマ必要:data-theme 戦略必須
- Next.js プロジェクト:next-themes 使用、attribute は需求で選択
- Astro プロジェクト:View Transitions の処理に絶対注意
実践スキル
白画面フラッシュ防止の完全ソリューション:
<head>
<script is:inline>
// 同期スクリプト、レンダリング前に実行
(function() {
const theme = localStorage.getItem('theme');
const systemDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
if (theme === 'dark' || (!theme && systemDark)) {
document.documentElement.classList.add('dark');
// 或は
document.documentElement.dataset.theme = 'dark';
}
})();
</script>
</head>
SSR プロジェクトの処理:
Next.js 等の SSR プロジェクトは hydration mismatch を避ける必要。next-themes は既にこの問題を処理済み。でも自分で実装したい場合、注意:
// useEffect で SSR 不整合を避ける
import { useEffect, useState } from 'react';
function useTheme() {
const [theme, setTheme] = useState('light');
useEffect(() => {
const saved = localStorage.getItem('theme');
setTheme(saved || 'light');
}, []);
return theme;
}
CSS 変数のセマンティック命名:
セマンティックな変数名を使用、色名じゃない:
/* 推奨 */
:root {
--background: ...;
--foreground: ...;
--primary: ...;
--muted: ...;
}
/* 不推奨 */
:root {
--white: ...;
--black: ...;
--gray-900: ...;
}
セマンティック命名はテーマ切り替えをより直感的に、後で新テーマ追加も便利。
まとめ
这么多話して、結局一句话にまとめる:class 戦略はシンプルで成熟、大部分のプロジェクトに適合;data-theme 戦略はセマンティクスが明確、マルチテーマシーンと CSS 変数深統合により適合。
Tailwind v4 の @custom-variant ディレクティブは両方の戦略の設定をシンプルで直感的にした。どちらを選ぶ、核心は需要を見る——shadcn/ui を使う場合、data-theme アプローチがより自然;シンプルなダークモード切り替えだけ必要、class 戦略は依然として信頼できる選択。
1つの詳細を忽略しない:フレームワーク統合時にこれらの穴を適切に処理、Astro の View Transitions と Next.js の SSR hydration 等。これらの詳細が処理不好、体験が差くなる。
FAQ
Tailwind v4 の @custom-variant と v3 の設定の違いは?
class と data-theme を同時に使用できる?
dark: 修饰符が多くてコードが冗長、どうする?
具体的做法:
1. globals.css で @theme 使用して変数を定義
2. 異なる [data-theme] 下で変数値をオーバーライド
3. tailwind.config.js でこれらの変数を参照
こうすると bg-primary はテーマ切り替えに自動適応。
Astro プロジェクトでダークモード状態が失われる、どうする?
document.addEventListener('astro:after-swap', () => {
const theme = localStorage.getItem('theme');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
});
このステップは多くの開発者が容易に漏れる。
ページ読み込み時の白画面フラッシュをどう解決?
<script>
if (localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
</script>
注意:スクリプトは同期必須、defer 或は async は使用不可。
参考資料
- Tailwind CSS Dark Mode 公式ドキュメント
- shadcn/ui Theming ドキュメント
- next-themes GitHub
- Astro Dark Mode with Tailwind
6 min read · 公開日: 2026年3月28日 · 更新日: 2026年3月28日
関連記事
shadcn/uiで管理画面の骨組みを構築:Sidebar + Layout ベストプラクティス
shadcn/uiで管理画面の骨組みを構築:Sidebar + Layout ベストプラクティス
Tailwind レスポンシブレイアウト実践:コンテナクエリとブレークポイント戦略
Tailwind レスポンシブレイアウト実践:コンテナクエリとブレークポイント戦略
Ubuntu 初期設定完全ガイド:ユーザー・SSH・fail2ban のセキュリティ設定

コメント
GitHubアカウントでログインしてコメントできます