Tailwind CSS のクラスが消える/縮む原因を特定:ツリーシェイクと safelist 完全対策
Tailwind CSS で開発していると、開発環境では正常に表示されていたスタイルが、本番ビルド後に突然消えてしまう経験はありませんか。
この問題は多くの開発者を悩ませていますが、実は Tailwind CSS の「ツリーシェイク」という仕組みが原因です。本記事では、なぜクラスが消えるのか、その原因を徹底的に解明し、safelist を使った確実な対策方法をご紹介します。
背景
Tailwind CSS のビルド最適化の仕組み
Tailwind CSS は、デフォルトで数千以上のユーティリティクラスを提供しています。しかし、実際のプロジェクトで使用するクラスはその一部に過ぎません。
そこで Tailwind CSS は、本番ビルド時に「使用されていないクラス」を自動的に削除する最適化を行います。この処理を ツリーシェイク(Tree Shaking) と呼びます。
以下の図は、Tailwind CSS のビルドプロセスを示したものです。
mermaidflowchart TB
source["ソースコード<br/>(HTML/JSX/Vue)"] -->|スキャン| scanner["Content Scanner"]
scanner -->|クラス名抽出| classes["使用クラス一覧"]
classes -->|フィルタリング| purge["PurgeCSS/JIT"]
purge -->|最適化| output["最終 CSS<br/>(数十 KB)"]
allClasses["全 Tailwind クラス<br/>(数 MB)"] -.->|使用分のみ| purge
style source fill:#e3f2fd
style output fill:#c8e6c9
style purge fill:#fff9c4
図の要点:
- ソースコード内のクラス名を静的にスキャン
- 検出されたクラスのみを CSS に含める
- 未使用クラスは自動的に削除される
JIT モードとスキャンの仕組み
Tailwind CSS v3 以降では、JIT(Just-In-Time)モードがデフォルトになりました。JIT モードは、ファイルを監視してクラス名を検出し、必要な CSS のみをリアルタイムで生成します。
javascript// tailwind.config.js の content 設定
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
};
上記の設定により、Tailwind は指定されたパターンに一致するファイルをスキャンし、そこに含まれるクラス名を検出するのです。
スキャン対象となるクラス名の条件
Tailwind のスキャナーは、以下のようなクラス名を検出できます。
javascript// ✓ 検出される例
<div className='bg-blue-500 text-white'>静的なクラス</div>;
const Card = () => (
<div className='p-4 rounded-lg shadow-md'>
完全な文字列として記述
</div>
);
しかし、次のような動的な記述は検出されません。
javascript// ✗ 検出されない例
const color = 'blue'
<div className={`bg-${color}-500`}>動的生成</div>
// ✗ 検出されない例
const buttonClass = isActive ? 'bg-green-500' : 'bg-gray-500'
<button className={buttonClass.split(' ')[0]}>ボタン</button>
このように、Tailwind のスキャナーは 静的な文字列としてのクラス名 のみを認識します。
課題
クラスが消える具体的なケース
実際の開発では、以下のようなケースでクラスが消えてしまいます。
ケース 1:テンプレートリテラルでの動的生成
typescript// components/Badge.tsx
type BadgeProps = {
color: 'red' | 'blue' | 'green';
children: React.ReactNode;
};
const Badge: React.FC<BadgeProps> = ({
color,
children,
}) => {
// ❌ 本番ビルドでクラスが消える
return (
<span
className={`bg-${color}-500 text-white px-2 py-1`}
>
{children}
</span>
);
};
上記のコードでは、bg-red-500、bg-blue-500、bg-green-500 が動的に生成されるため、Tailwind のスキャナーが検出できません。
ケース 2:外部ライブラリや CMS からの動的クラス
typescript// pages/article/[id].tsx
import { getArticle } from '@/lib/cms';
export default function ArticlePage({ article }) {
// ❌ CMS から取得したクラス名は検出されない
return (
<div className={article.customClass}>
{/* article.customClass = "bg-purple-500 text-lg" など */}
<h1>{article.title}</h1>
</div>
);
}
CMS(Contentful、microCMS など)から取得したクラス名は、ビルド時には存在しないため検出されません。
ケース 3:条件分岐での複雑な組み合わせ
typescript// components/Alert.tsx
type AlertType = 'success' | 'warning' | 'error' | 'info';
const Alert = ({
type,
message,
}: {
type: AlertType;
message: string;
}) => {
const colorMap = {
success: 'green',
warning: 'yellow',
error: 'red',
info: 'blue',
};
// ❌ オブジェクトから取得したクラス名は検出されない
const bgColor = `bg-${colorMap[type]}-100`;
const textColor = `text-${colorMap[type]}-800`;
return (
<div className={`${bgColor} ${textColor} p-4 rounded`}>
{message}
</div>
);
};
上記のように、オブジェクトを介してクラス名を生成する場合も検出されません。
なぜ検出されないのか
以下の図は、Tailwind のスキャナーがクラス名を検出できない仕組みを示しています。
mermaidflowchart LR
code["コード"] -->|スキャン| scanner["正規表現マッチング"]
scanner -->|完全一致| detected["検出成功"]
scanner -->|部分文字列| notDetected["検出失敗"]
detected --> build["CSS に含まれる"]
notDetected --> purged["CSS から削除"]
style detected fill:#c8e6c9
style notDetected fill:#ffcdd2
style purged fill:#ffcdd2
図の要点:
- スキャナーは正規表現で完全なクラス名を探す
- テンプレートリテラルは文字列として認識されない
- 検出されないクラスは本番ビルドで削除される
Tailwind のスキャナーは、以下のような正規表現パターンでクラス名を検出します。
javascript// Tailwind の内部的なスキャンパターン(簡略版)
const classPattern = /[^<>"'`\s]*[^<>"'`\s:]/g;
このパターンは、文字列リテラルとして完全に記述されたクラス名のみをマッチさせます。したがって、`bg-${color}-500` のような部分的な文字列は検出できないのです。
開発環境では動作する理由
開発環境では、すべてのクラスが含まれたフル版の CSS が読み込まれます。一方、本番ビルドでは最適化されたミニマム版の CSS のみが含まれます。
| # | 環境 | CSS サイズ | 含まれるクラス |
|---|---|---|---|
| 1 | 開発 | 数 MB | 全クラス |
| 2 | 本番 | 数十 KB | 使用クラスのみ |
これが、開発環境では正常に表示されるのに、本番環境でスタイルが消える理由です。
解決策
safelist を使った確実な対策
safelist は、スキャンで検出されなくても強制的に CSS に含めるクラス名を指定できる機能です。これにより、動的に生成されるクラスも確実に本番ビルドに含められます。
基本的な safelist の設定
javascript// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
// safelist 設定を追加
safelist: ['bg-red-500', 'bg-blue-500', 'bg-green-500'],
};
上記の設定により、指定した 3 つのクラスは必ず CSS に含まれます。
正規表現パターンでの一括指定
個別にクラスを列挙するのは大変です。正規表現パターンを使えば、関連するクラスをまとめて指定できます。
javascript// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
safelist: [
// 背景色:red, blue, green の 100〜900 すべて
{
pattern:
/bg-(red|blue|green)-(100|200|300|400|500|600|700|800|900)/,
},
// テキスト色も同様に
{
pattern:
/text-(red|blue|green)-(100|200|300|400|500|600|700|800|900)/,
},
],
};
この設定により、bg-red-100 から bg-green-900 まで、すべての組み合わせが CSS に含まれます。
バリアント(hover、focus など)も含める
Tailwind CSS では、hover:、focus:、dark: などのバリアントも使用できます。safelist でこれらも指定しましょう。
javascript// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
safelist: [
{
pattern: /bg-(red|blue|green)-(100|500|900)/,
// hover と focus バリアントも含める
variants: ['hover', 'focus'],
},
],
};
これにより、hover:bg-red-500 や focus:bg-blue-500 なども CSS に含まれます。
コンポーネント単位での safelist 管理
プロジェクトが大きくなると、safelist も肥大化します。コンポーネントごとに必要なクラスを明確にしましょう。
javascript// tailwind.config.js
// Badge コンポーネント用のクラス
const badgeSafelist = [
{
pattern: /bg-(red|blue|green|yellow|purple)-(500)/,
},
{
pattern: /text-white/,
},
];
// Alert コンポーネント用のクラス
const alertSafelist = [
{
pattern: /bg-(green|yellow|red|blue)-(100)/,
},
{
pattern: /text-(green|yellow|red|blue)-(800)/,
},
];
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
safelist: [...badgeSafelist, ...alertSafelist],
};
このように分類することで、どのコンポーネントがどのクラスを使用しているか管理しやすくなります。
CMS や外部データ用の safelist
CMS から取得するクラス名は予測できないため、幅広く safelist に含める必要があります。
javascript// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
safelist: [
// CMS で使用する可能性のあるすべての背景色
{
pattern:
/bg-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|200|300|400|500|600|700|800|900|950)/,
},
// テキストサイズ
{
pattern:
/text-(xs|sm|base|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)/,
},
// パディング・マージン
{
pattern:
/(p|m|px|py|pt|pb|pl|pr|mx|my|mt|mb|ml|mr)-(0|1|2|3|4|5|6|8|10|12|16|20|24)/,
},
],
};
上記の設定により、CMS から動的に挿入されるクラスも本番ビルドに含まれます。
具体例
実装例 1:Badge コンポーネントの修正
問題のあったコードを safelist で解決します。
Before(クラスが消える)
typescript// components/Badge.tsx
type BadgeProps = {
color: 'red' | 'blue' | 'green';
children: React.ReactNode;
};
const Badge: React.FC<BadgeProps> = ({
color,
children,
}) => {
// ❌ 動的生成のため検出されない
return (
<span
className={`bg-${color}-500 text-white px-2 py-1 rounded`}
>
{children}
</span>
);
};
export default Badge;
After(safelist で対応)
まず、tailwind.config.js に safelist を追加します。
javascript// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
safelist: [
// Badge コンポーネント用のクラス
'bg-red-500',
'bg-blue-500',
'bg-green-500',
'text-white',
'px-2',
'py-1',
'rounded',
],
};
これで Badge コンポーネントは本番ビルドでも正常に動作します。
さらに良いアプローチ:静的クラスマップを使用
safelist に頼るだけでなく、コード側でも静的にクラスを定義することで、Tailwind のスキャナーが検出できるようにします。
typescript// components/Badge.tsx
type BadgeColor = 'red' | 'blue' | 'green';
type BadgeProps = {
color: BadgeColor;
children: React.ReactNode;
};
// ✓ 静的なクラスマップ(スキャナーが検出可能)
const colorClasses: Record<BadgeColor, string> = {
red: 'bg-red-500',
blue: 'bg-blue-500',
green: 'bg-green-500',
};
const Badge: React.FC<BadgeProps> = ({
color,
children,
}) => {
// ✓ オブジェクトから完全なクラス名を取得
return (
<span
className={`${colorClasses[color]} text-white px-2 py-1 rounded`}
>
{children}
</span>
);
};
export default Badge;
このアプローチでは、colorClasses オブジェクト内のクラス名が文字列リテラルとして記述されているため、Tailwind のスキャナーが検出できます。
実装例 2:Alert コンポーネントの修正
より複雑な Alert コンポーネントも同様に対応できます。
完全なクラスマップの定義
typescript// components/Alert.tsx
import React from 'react';
type AlertType = 'success' | 'warning' | 'error' | 'info';
type AlertProps = {
type: AlertType;
message: string;
};
// ✓ すべてのクラスを静的に定義
const alertStyles: Record<
AlertType,
{ bg: string; text: string; border: string }
> = {
success: {
bg: 'bg-green-100',
text: 'text-green-800',
border: 'border-green-300',
},
warning: {
bg: 'bg-yellow-100',
text: 'text-yellow-800',
border: 'border-yellow-300',
},
error: {
bg: 'bg-red-100',
text: 'text-red-800',
border: 'border-red-300',
},
info: {
bg: 'bg-blue-100',
text: 'text-blue-800',
border: 'border-blue-300',
},
};
const Alert: React.FC<AlertProps> = ({ type, message }) => {
const styles = alertStyles[type];
// ✓ 完全なクラス名を使用
return (
<div
className={`${styles.bg} ${styles.text} ${styles.border} border-l-4 p-4 rounded`}
>
<p className='font-semibold'>{message}</p>
</div>
);
};
export default Alert;
上記のコードでは、すべてのクラスが静的に定義されているため、safelist なしでも検出されます。
念のための safelist 設定
万全を期すために、safelist も設定しておきましょう。
javascript// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
safelist: [
// Alert コンポーネント用(念のため)
{
pattern: /bg-(green|yellow|red|blue)-(100)/,
},
{
pattern: /text-(green|yellow|red|blue)-(800)/,
},
{
pattern: /border-(green|yellow|red|blue)-(300)/,
},
],
};
実装例 3:CMS データの動的クラス対応
外部 CMS からクラス名を取得する場合は、safelist が必須です。
CMS から取得するデータ構造
typescript// types/article.ts
export type Article = {
id: string;
title: string;
content: string;
customStyles: {
containerClass: string;
headingClass: string;
textClass: string;
};
};
コンポーネントでの使用
typescript// components/CMSArticle.tsx
import React from 'react';
import type { Article } from '@/types/article';
type CMSArticleProps = {
article: Article;
};
const CMSArticle: React.FC<CMSArticleProps> = ({
article,
}) => {
// CMS から取得した動的クラス名を使用
return (
<article
className={article.customStyles.containerClass}
>
<h1 className={article.customStyles.headingClass}>
{article.title}
</h1>
<div className={article.customStyles.textClass}>
{article.content}
</div>
</article>
);
};
export default CMSArticle;
CMS 用の包括的な safelist
javascript// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
safelist: [
// すべての色の背景(50〜950)
{
pattern:
/bg-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|200|300|400|500|600|700|800|900|950)/,
},
// すべての色のテキスト
{
pattern:
/text-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|200|300|400|500|600|700|800|900|950)/,
},
// テキストサイズ全種類
'text-xs',
'text-sm',
'text-base',
'text-lg',
'text-xl',
'text-2xl',
'text-3xl',
'text-4xl',
'text-5xl',
// パディング(0〜24)
{
pattern:
/(p|px|py|pt|pb|pl|pr)-(0|1|2|3|4|5|6|8|10|12|16|20|24|32)/,
},
// マージン(0〜24)
{
pattern:
/(m|mx|my|mt|mb|ml|mr)-(0|1|2|3|4|5|6|8|10|12|16|20|24|32)/,
},
// ボーダー・角丸
{
pattern: /rounded-(none|sm|md|lg|xl|2xl|3xl|full)/,
},
{
pattern: /border-(0|2|4|8)/,
},
],
};
この設定により、CMS から取得されるほぼすべてのクラスが本番ビルドに含まれます。
デバッグ方法:どのクラスが削除されたか確認する
Tailwind CSS の設定で、削除されたクラスをログ出力できます。
javascript// tailwind.config.js
module.exports = {
content: [
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
safelist: [
// safelist の設定...
],
// デバッグオプション
future: {
removeDeprecatedGapUtilities: true,
purgeLayersByDefault: true,
},
// ビルド時の詳細ログ
corePlugins: {
preflight: true,
},
};
また、ビルド時に以下のコマンドでデバッグ情報を出力できます。
bash# ビルド時に詳細なログを出力
NODE_ENV=production npx tailwindcss -i ./styles/globals.css -o ./dist/output.css --verbose
出力された CSS ファイルを確認することで、どのクラスが含まれているか検証できます。
本番ビルドのテスト手順
開発環境と本番環境の差を確認するには、以下の手順でテストします。
ステップ 1:本番ビルドの実行
bash# Next.js の場合
yarn build
# 静的エクスポートの場合
yarn build && yarn export
ステップ 2:本番環境のプレビュー
bash# Next.js の場合
yarn start
# 静的ファイルの場合
npx serve out
ステップ 3:CSS ファイルの確認
ビルドされた CSS ファイルを直接確認します。
bash# ビルドされた CSS を検索
find .next -name "*.css" -type f
# CSS 内のクラスを検索
grep "bg-red-500" .next/static/css/*.css
クラスが見つからない場合は、safelist に追加が必要です。
まとめ
Tailwind CSS でクラスが消える問題は、ツリーシェイクによる最適化が原因です。この問題は、safelist を適切に設定することで確実に解決できます。
本記事でご紹介した対策をまとめます。
| # | 対策方法 | 適用ケース | メリット |
|---|---|---|---|
| 1 | safelist で個別指定 | 少数の動的クラス | シンプルで確実 |
| 2 | safelist で正規表現パターン | 多数の関連クラス | 一括指定が可能 |
| 3 | 静的クラスマップ | コンポーネント内の動的クラス | スキャナーが検出可能 |
| 4 | CMS 用の包括的 safelist | 外部データの動的クラス | すべてのケースをカバー |
重要なポイントは以下の通りです。
動的に生成されるクラス名は、Tailwind のスキャナーが検出できないため、safelist での明示的な指定が必要になります。コンポーネント内で使用するクラスは、可能な限り静的な文字列リテラルとして定義することで、safelist なしでも検出されるようになるでしょう。
CMS や外部 API から取得するクラス名は、包括的な safelist 設定が不可欠です。本番ビルド前に必ずテストを行い、スタイルが正しく適用されているか確認してください。
これらの対策を実践することで、Tailwind CSS のクラスが消える問題を根本から解決できます。安心して動的なスタイリングを実装し、ユーザーに一貫した UI を提供しましょう。
関連リンク
articleTailwind CSS のクラスが消える/縮む原因を特定:ツリーシェイクと safelist 完全対策
articleTailwind CSS 2025 年ロードマップ総ざらい:新機能・互換性・移行の見取り図
articleTailwind CSS 運用監視:eslint-plugin-tailwindcss でクラスミスを未然に防ぐ
articleTailwind CSS マルチブランド設計:CSS 変数と data-theme で横断対応
articleTailwind CSS バリアント辞典:aria-[]/data-[]/has-[]/supports-[] を一気に使い倒す
articleTailwind CSS を macOS で最短導入:Yarn PnP・PostCSS・ESLint 連携レシピ
articlePlaywright Debug モード活用:テストが落ちる原因を 5 分で特定する手順
articleVue.js でメモリリーク?watch/effect/イベント登録の落とし穴と検知法
articleTailwind CSS のクラスが消える/縮む原因を特定:ツリーシェイクと safelist 完全対策
articlePHP 構文チートシート:配列・クロージャ・型宣言・match を一枚で把握
articleSvelte ストアエラー「store is not a function」を解決:writable/derived の落とし穴
articleNext.js の 観測可能性入門:OpenTelemetry/Sentry/Vercel Analytics 連携
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来