言語を切り替える
テーマを切り替える

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 メディアクエリを使って、ユーザーのシステムダークモード設定を自動検出。

<!-- 設定不要、システム設定に自動追従 -->
&lt;div class="bg-white dark:bg-gray-900"&gt;
  コンテンツはシステム設定に基づいて自動切り替え
&lt;/div&gt;

メリットは明確:ゼロ設定、ユーザーは何もしなくて好みの表示が得られる。でもデメリットも痛い——ユーザーが自分で選べない。ライト環境でダークモードを使いたい人にとって、体験が不足。

Class 戦略:手動切り替え制御

Class 戦略は親要素(通常 &lt;html&gt;)に .dark クラスを追加してダークモードをトリガー。開発者が完全な制御権を持てる、ユーザーの手動切り替えと設定の永続化が可能。

<!-- JavaScript でクラス名を制御 -->
&lt;html class="dark"&gt;
  &lt;body class="bg-white dark:bg-gray-900"&gt;
    ダークモード有効
  &lt;/body&gt;
&lt;/html&gt;

今最も広く使われているアプローチ。コミュニティドキュメントが豊富、各種サードパーティライブラリとの統合もスムーズ。

Data-theme 戦略:セマンティック属性セレクタ

Data-theme 戦略は data-theme="dark" 属性を使う、クラス名じゃない。セマンティクスがより明確、天然でマルチテーマ拡張をサポート。

&lt;html data-theme="dark"&gt;
  &lt;body class="bg-white dark:bg-gray-900"&gt;
    ダークモード有効
  &lt;/body&gt;
&lt;/html&gt;

より多くのテーマへの拡張が特に簡単——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 はデフォルトのライトモードでレンダリング済み。

解決策は &lt;head&gt; に同期実行スクリプトを配置、DOM レンダリング前にテーマを設定:

&lt;head&gt;
  &lt;script&gt;
    // 同期実行、フラッシュ防止
    if (localStorage.theme === 'dark' ||
        (!('theme' in localStorage) &&
         window.matchMedia('(prefers-color-scheme: dark)').matches)) {
      document.documentElement.classList.add('dark');
    }
  &lt;/script&gt;
&lt;/head&gt;

このスクリプトは同期必須——deferasync は使えない。

メリット・デメリット

メリット:

  • 実装がシンプルで直感的、早く習得できる
  • コミュニティリソースが豊富、各種フレームワークに成熟したソリューション
  • 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 モード、読書モードの定義がシンプル:

&lt;html data-theme="oled"&gt;
  <!-- 真黒背景、OLED スクリーンに適合 -->
&lt;/html&gt;

&lt;html data-theme="sepia"&gt;
  <!-- 淡黄色背景、読書に適合 -->
&lt;/html&gt;

切り替えロジックは属性値を変更だけ:

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 実装複雑度
属性セレクタ理解必要
Class コミュニティサポート
ドキュメント豊富
Data-theme コミュニティサポート
普及中
困難
Class マルチテーマ
複数クラス名必要
容易
Data-theme マルチテーマ
属性値変更のみ
数据来源: 戦略比較分析
比較维度Class 戦略Data-theme 戦略
実装複雑度低、設定シンプル中、属性セレクタ理解必要
セマンティクス明確度中、.dark の意味は考える必要高、data-theme 直感
マルチテーマ拡張困難、複数クラス名必要容易、属性値変更のみ
コミュニティサポート高、ドキュメント豊富中、普及中
CSS 変数統合追加適応必要天然フレンドリー
Tailwind v3darkMode: '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()]
  }
});

ダークモードスクリプト:

&lt;!-- BaseLayout.astro の head に配置 --&gt;
&lt;script is:inline&gt;
  // フラッシュ防止の同期スクリプト
  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';
  }
&lt;/script&gt;

View Transitions 处理:

Astro の View Transitions はページ切り替え時に DOM を再レンダリング、ダークモード状態が容易に失われる。astro:after-swap イベントを監視してテーマを再設定:

&lt;script&gt;
  document.addEventListener('astro:after-swap', () => {
    const theme = localStorage.getItem('theme');
    if (theme === 'dark') {
      document.documentElement.classList.add('dark');
    }
  });
&lt;/script&gt;

このステップはかなり重要——多くの開発者が容易に漏れる、私もこの穴を踏んだ。

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 (
    &lt;ThemeProvider
      attribute="class"        // class 戦略使用
      defaultTheme="system"    // デフォルトシステム追従
      enableSystem={true}      // システム検出有効
      disableTransitionOnChange  // 切り替え時フラッシュ防止
    &gt;
      {children}
    &lt;/ThemeProvider&gt;
  );
}

data-theme 戦略に切り替え?attribute プロパティを変更だけ:

&lt;ThemeProvider attribute="data-theme" defaultTheme="system"&gt;

Layout で使用:

// app/layout.tsx
import { ThemeProvider } from './components/ThemeProvider';

export default function RootLayout({ children }) {
  return (
    &lt;html lang="ja"&gt;
      &lt;body&gt;
        &lt;ThemeProvider&gt;
          {children}
        &lt;/ThemeProvider&gt;
      &lt;/body&gt;
    &lt;/html&gt;
  );
}

切り替えボタンコンポーネント:

// components/ThemeToggle.tsx
import { useTheme } from 'next-themes';

export function ThemeToggle() {
  const { theme, setTheme } = useTheme();

  return (
    &lt;button
      onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}
      className="p-2 rounded-lg"
    &gt;
      {theme === 'dark' ? '☀️' : '🌙'}
    &lt;/button&gt;
  );
}

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);
}

そして直接これらの色を使用:

&lt;button class="bg-primary text-white"&gt;ボタン&lt;/button&gt;

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';
    }
  });

こうすると、ユーザーは固定テーマを選択、或は常にシステム追従が可能。


ベストプラクティスまとめ

推奨戦略選択

大部分のプロジェクトに対して、私のアドバイスは:

  1. シンプルプロジェクト:class 戦略使用、シンプルな切り替えスクリプトで十分
  2. shadcn/ui 使用:直接 data-theme + CSS 変数アプローチ
  3. マルチテーマ必要:data-theme 戦略必須
  4. Next.js プロジェクト:next-themes 使用、attribute は需求で選択
  5. Astro プロジェクト:View Transitions の処理に絶対注意

実践スキル

白画面フラッシュ防止の完全ソリューション:

&lt;head&gt;
  &lt;script is:inline&gt;
    // 同期スクリプト、レンダリング前に実行
    (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';
      }
    })();
  &lt;/script&gt;
&lt;/head&gt;

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 の設定の違いは?
主要な違いは設定位置。v3 は JS 設定ファイル(tailwind.config.js)で定義、v4 は CSS ファイルで @custom-variant ディレクティブで宣言。機能は完全に同じ、v4 のアプローチは "CSS-first" デザイン理念により適合。
class と data-theme を同時に使用できる?
可能、でも必要ない。両者は機能的に完全に同じ、同時使用は複雑度を増加。shadcn/ui は .dark クラスと [data-theme="dark"] 属性を同時にサポート——異なるユーザーの習慣に対応、どちらかを選択すればOK。
dark: 修饰符が多くてコードが冗長、どうする?
CSS 変数アプローチを使用。変数を定義後、属性値を切り替えるだけで、変数を使用する全スタイルが自動更新——各要素に dark: 修饰符を書く必要ない。

具体的做法:
1. globals.css で @theme 使用して変数を定義
2. 異なる [data-theme] 下で変数値をオーバーライド
3. tailwind.config.js でこれらの変数を参照

こうすると bg-primary はテーマ切り替えに自動適応。
Astro プロジェクトでダークモード状態が失われる、どうする?
Astro の View Transitions はページ切り替え時に DOM を再レンダリング、ダークモード状態が容易に失われる。解決方法は astro:after-swap イベントを監視してテーマを再設定:

document.addEventListener('astro:after-swap', () => {
const theme = localStorage.getItem('theme');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
});

このステップは多くの開発者が容易に漏れる。
ページ読み込み時の白画面フラッシュをどう解決?
&lt;head&gt; に同期実行スクリプトを配置、DOM レンダリング前にテーマを設定:

&lt;script&gt;
if (localStorage.theme === 'dark' ||
(!('theme' in localStorage) &&
window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
}
&lt;/script&gt;

注意:スクリプトは同期必須、defer 或は async は使用不可。

参考資料

6 min read · 公開日: 2026年3月28日 · 更新日: 2026年3月28日

コメント

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

関連記事