Tailwind CSS の dark モードが切り替わらない/適用されない
darkMode の設定値(media / class / selector)と HTML 側のトグル方法がずれているのが大半の原因。
Tailwind v4 は CSS-first 設定(@variant dark)に変わったため、バージョン確認も同時に行う。
公開: 更新:
要約
Tailwind の dark モードが効かないとき、原因はほぼ次の 4 つに収まる: 既定の media モードで OS 追従になっている、dark クラスを HTML に付与し忘れている、v3 と v4 で設定方法が違うのに混在している、dark: プレフィックスが JIT スキャナで検出されていない。
バージョンを確認したうえで設定を v3 / v4 のどちらかに統一する。
実行例
darkMode を書かない既定のままビルドすると、dark:bg-slate-900 は @media (prefers-color-scheme: dark) の中にしか出力されず、html に dark クラスを付けても発火しない状態だった。
設定に darkMode: 'class' を足して再ビルドすると同じクラスが .dark\:bg-slate-900:is(.dark *) というセレクタに変わり、祖先の .dark に反応する形になった。
一方で変数から組み立てた dark: クラスは完全な文字列としてソースに現れないため、出力を blue-900 で grep しても 0 件のままだった。
$ npx tailwindcss -i src/input.css -o out.css
Browserslist: caniuse-lite is outdated. Please run:
npx update-browserslist-db@latest
Why you should do it regularly: https://github.com/browserslist/update-db#readme
Rebuilding...
Done in 96ms.$ grep -B1 -A3 -F dark\:bg-slate-900 out.css
@media (prefers-color-scheme: dark) {
.dark\:bg-slate-900 {
--tw-bg-opacity: 1;
background-color: rgb(15 23 42 / var(--tw-bg-opacity, 1));
}
prefers-color-scheme のメディアクエリ内にしか出ない = html に dark を付けても発火しない$ npx tailwindcss -i src/input.css -o out.css
Browserslist: caniuse-lite is outdated. Please run:
npx update-browserslist-db@latest
Why you should do it regularly: https://github.com/browserslist/update-db#readme
Rebuilding...
Done in 99ms.$ grep -B1 -A3 -F dark\:bg-slate-900 out.css
.dark\:bg-slate-900:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(15 23 42 / var(--tw-bg-opacity, 1));
}
.dark を祖先に持つときだけ効くセレクタに変わった$ npx tailwindcss -i src/input.css -o out.css
Browserslist: caniuse-lite is outdated. Please run:
npx update-browserslist-db@latest
Why you should do it regularly: https://github.com/browserslist/update-db#readme
Rebuilding...
Done in 100ms.$ grep -c blue-900 out.css
0
0 = 変数補完した dark: クラスは検出されない$ npx tailwindcss --help
tailwindcss v3.4.19
Usage:
tailwindcss build [options]
Options:
-i, --input Input file
-o, --output Output file
-w, --watch Watch for changes and rebuild as needed
-p, --poll Use polling instead of filesystem events when watching
--content Content paths to use for removing unused classes
--postcss Load custom PostCSS configuration
-m, --minify Minify the output
-c, --config Path to a custom config file
--no-autoprefixer Disable autoprefixer
-h, --help Display usage information— 2026-08-26 時点の出力
検証環境
- 検証日
- 実行環境
node:20Debian GNU/Linux 12 (bookworm)- バージョン
- Node.js 20.20.2
- npm 10.8.2
- Python 3.11.2
- Git 2.39.5
この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。
よくある原因
- 既定
mediaモード:darkModeを書かないと OS のprefers-color-schemeに追従するだけで、手動トグルは反応しない。 darkクラス未付与:darkMode: 'class'にしても、<html class="dark">のように親要素にクラスを付けない限りdark:*ユーティリティは発火しない。- v3 / v4 混在: Tailwind v4 は CSS-first 設定に移行しており、
tailwind.config.jsのdarkModeではなく CSS の@variant darkで定義する。
v4 で v3 設定を書いても無視される。 - JIT 検出漏れ:
dark:bg-{color}を変数補完で組み立てると完全クラス名がソース上に出てこず、出力 CSS に含まれない。
解決策
1. v3 / v4 で設定を書き分ける
// Tailwind v3: tailwind.config.js
module.exports = {
content: ["./src/**/*.{ts,tsx,html}"],
darkMode: "class",
};/* Tailwind v4: src/app.css */
@import "tailwindcss";
@variant dark (.dark &);Dark Mode 公式ドキュメント(新しいタブで開く) のとおり、v3 は JS 側、v4 は CSS 側で宣言する。
バージョンは npx tailwindcss --help の先頭行で確認できる。
2. ルート要素にクラスを付与する
// app/providers.tsx
"use client";
import { useEffect, useState } from "react";
export function ThemeToggle() {
const [dark, setDark] = useState(false);
useEffect(() => {
document.documentElement.classList.toggle("dark", dark);
localStorage.setItem("theme", dark ? "dark" : "light");
}, [dark]);
return <button onClick={() => setDark((v) => !v)}>toggle</button>;
}SSR の初期表示でフラッシュを避けたければ、<head> 内のインライン script で localStorage を読んで先に dark クラスを当てる定番パターンを使う。
3. 完全クラス名で書く
// NG: スキャンで検出されない
<div className={`dark:bg-${color}-900`} />
// OK: 完全文字列を分岐
<div className={dark ? "dark:bg-blue-900" : "dark:bg-red-900"} />dark: プレフィックス付きでも JIT の規則は同じで、完全文字列がソース上に存在する必要がある。content の glob に該当ファイルが含まれていることも合わせて確認する(Upgrade Guide(新しいタブで開く) も参照)。