
Tailwind CSSで動的クラスが反映されない原因と@source inline()の考え方
Tailwind CSSでDBや変数から生成した動的クラスが反映されない原因を解説します。Tailwindのクラス検出の仕組みと、v4で使える@source inline()による対策を実例つきで紹介します。
シリーズ:Tailwind CSS v4 入門
1
v3→v4 変更点まとめ
2
TS + Next.js / Bun セットアップ
3
@theme でデザイントークン管理
4
レスポンシブ・ダークモード実践
5
動的クラスと @source inline()
6
コンポーネント設計パターン集
「開発環境では表示されていたのに、本番ビルド後にスタイルが消えてしまった」。Tailwind CSS を使っていると、ほぼ確実に一度はぶつかる壁です。
このバグはランダムに起きているわけではありません。 Tailwind のビルドの仕組みを理解すれば、なぜ起きるか・どう防ぐかがはっきり見えてきます。 今回はその仕組みを根本から解説し、v4 の正しい解決策である@source inline()まで丁寧に説明します。
🎯 この記事のゴール
「動的クラスがなぜ消えるのか」をビルドパイプラインのレベルで理解し、@source inline() を使って正しく解決できるようになります。また、状況に応じた解決策の選び方と、そもそも問題を起こさないコードの書き方も身につけます。
Tailwind がどうやってクラスを収集しているか
問題を理解するには、Tailwind がどうやってスタイルを生成しているかを知る必要があります。Tailwind はすべての CSS クラスをあらかじめ出力するわけではありません。
「実際にソースファイル内に書かれているクラスだけを検出して、そのCSSだけを出力する」 というアプローチを採っています。これが Tailwind の CSS ファイルサイズを小さく保てる理由です。
Tailwind のビルドパイプライン
🔍
① ソースファイルをスキャン
.tsx / .jsx / .html などのファイルを読み取り、className="..." やclass="..." の中に書かれた文字列を収集する。v4 では.gitignore を参照しながらプロジェクト全体を自動スキャン。
⚙️
② 収集したクラス名から CSS を生成
スキャンで見つかったクラス名のリストを元に、対応する CSS ルールだけを生成する。見つからなかったクラスの CSS は一切生成されない。
📦
③ 最小サイズの CSS を出力
使われているクラスの CSS だけが含まれた、最小化されたスタイルシートが出力される。これが本番環境に配信される CSS。
⚠️
問題が起きる場所
スキャン時点でクラス名が「文字列として存在しない」場合、そのクラスの CSS は生成されない。 JavaScript の計算結果として初めてクラス名が決まるパターン がこれに当たる。
💡 スキャンは「静的テキスト検索」
Tailwind のスキャンは JavaScript を実行せず、ソースファイルを テキストとして読み取るだけ です。
つまり「ファイルのどこかにbg-red-500 という文字列が存在するか」だけを見ています。変数や条件式の評価は行われません。
問題が起きる具体的なパターン
どんなコードを書くとスタイルが消えるのか、よくある3つのパターンを確認しましょう。
パターン①:文字列の結合(最も多いケース)
// color という変数に "red" や "blue" が入る
const color = "red";
// ⚠ ビルド時に "bg-red-500" という文字列がファイル上に存在しない!
// Tailwind が見るのは `bg-${color}-500` という文字列だけ
<div className={`bg-${color}-500`}>...</div>
// 同様にNG:オブジェクトから取り出す場合
const colorMap = { error: "red", success: "green" };
<div className={`bg-${colorMap[type]}-500`}>...</div> パターン②:条件式での部分的な結合
// prefix だけ変数にする場合も同様にNG
const size = "lg";
<button className={`text-${size} px-${size}`}>ボタン</button>
// → "text-lg", "px-lg" がビルド後に存在しない
// 後ろだけ変数にするパターンも同様
const level = 500;
<div className={`text-blue-${level}`}>...</div> パターン③:外部データから動的に決まる場合
// API や props から className がそのまま渡ってくる場合
interface BadgeProps {
colorClass: string; // "bg-green-500" などが渡る想定
}
function Badge({ colorClass }: BadgeProps) {
// 外部から渡された文字列はスキャン時に存在しないことがある
return <span className={colorClass}>バッジ</span>;
}
// CMSやデータベースから取得した className を直接当てるケース
// <div className={post.themeClass}> のような使い方 スキャンの動きを体感する:インタラクティブデモ
Tailwind がソースを「テキストとして見ている」様子をイメージしたデモです。左のコードでスキャンされるクラスと、スキャンされないクラスの違いを確認してください。
📄 ソースコード
function Card() { return ( < div className = "rounded-lg border p-6 bg-white text-gray-900" > < h2 className = "text-xl font-bold" > タイトル </ h2 > </ div > ); }
🔍 スキャン結果
✓ rounded-lg ✓ border ✓ p-6 ✓ bg-white ✓ text-gray-900 ✓ text-xl ✓ font-bold
✓ すべてのクラスが検出されました
解決策は4つある:まず全体像を把握する
この問題への解決策は4つありますが、v4 では推奨される方法が変わっています。それぞれの特徴と使いどころを確認してから詳細に進みましょう。
SOLUTION 01
完全なクラス名を静的に書く
コードの書き方を変える。動的にしたい場合は完全なクラス名をオブジェクト・配列で管理する。
✓ 最推奨・根本解決
SOLUTION 02
@source inline()
v4 の新機能。生成したいクラスのパターンを CSS に書き、Tailwind にその CSS を強制出力させる。
✓ v4 推奨・動的パターン向け
SOLUTION 03
@source でファイル追加
v4 の新機能。生成したいクラスのパターンを CSS に書き、Tailwind にその CSS を強制出力させる。
✓ v4 推奨・動的パターン向け
SOLUTION 04
safelist(v3 の方法)
v3 のtailwind.config.js で設定していた方法。v4 では設定ファイルがないため直接は使えない。
✗ v4 非対応
Solution 01:完全なクラス名を静的に書く(最推奨)
最も確実で、パフォーマンスへの影響もゼロの解決策です。「動的にしたい」という場面でも、 完全なクラス名をオブジェクトや配列で管理する 書き方に変えるだけで解決します。
基本パターン:オブジェクトマップで管理する
// NG例 : 文字列を結合している
const color = "red";
<div className={`bg-${color}-500 text-${color}-700`}>...</div>// OK例 : 完全なクラス名をオブジェクトで管理する
// 完全なクラス名を値にもつオブジェクト(Tailwind がスキャンで検出できる)
const colorMap = {
red: "bg-red-500 text-red-700",
green: "bg-green-500 text-green-700",
blue: "bg-blue-500 text-blue-700",
} as const;
const color: keyof typeof colorMap = "red";
<div className={colorMap[color]}>...</div>💡 なぜこれで解決するのか
Tailwind のスキャンはファイルを「テキストとして読む」だけです。colorMap の値として"bg-red-500" という 完全な文字列がソースファイルのどこかに書かれていれば 、それは検出されます。変数に代入されていようがオブジェクトの値であろうが、文字列として存在していれば問題ありません。
よく使うパターン:バリアントコンポーネント
// ✅ 各バリアントの完全なクラス名を定数で管理する
const variantClasses = {
primary: "bg-brand-500 hover:bg-brand-700 text-white",
secondary: "bg-gray-100 hover:bg-gray-200 text-gray-900",
danger: "bg-red-500 hover:bg-red-700 text-white",
ghost: "bg-transparent hover:bg-gray-100 text-gray-700 border border-gray-300",
} as const;
const sizeClasses = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "px-6 py-3 text-lg",
} as const;
interface ButtonProps {
variant?: keyof typeof variantClasses;
size?: keyof typeof sizeClasses;
children: React.ReactNode;
}
export function Button({
variant = "primary",
size = "md",
children,
}: ButtonProps) {
return (
<button
className={`
rounded-lg font-medium transition-colors
${variantClasses[variant]}
${sizeClasses[size]}
`}
>
{children}
</button>
);
} Solution 02:@source inline() を使う(v4 の新機能)
コードの書き方を変えられない場合や、どうしても動的に全パターンを網羅したい場合は@source inline() を使います。これは v4 で新たに追加された機能です。
@source inline() の仕組み
@source inline() は「 この文字列パターンから生成されうるすべてのクラス CSS を強制的に出力する 」という命令です。Tailwind の内部で使われているパターンマッチング構文(ブレース展開)を使って、一度に複数のクラスを指定できます。
@source inline() 入力パターン → 生成されるクラス
"bg-red-500"
→
"bg-red-500"
完全なクラス名を1つ指定した場合。そのクラスの CSS が確実に出力される。
"bg-{red,green,blue}-500"
→
"bg-red-500"
"bg-green-500"
"bg-blue-500"
ブレース展開で複数の色を指定。{A,B,C} はそれぞれの組み合わせを展開する。
"bg-{red,green,blue}-{100,500,700}"
→
"bg-red-100"
"bg-red-500"
"bg-red-700"
・・・
"bg-red-700"
色×濃淡の組み合わせを一括展開。3色×3濃淡=9クラスの CSS が生成される。
実際の書き方:CSS ファイルに追記する
@import "tailwindcss";
@theme {
--color-brand-500: #0ea5e9;
/* ... */
}
/*
* @source inline() で生成したいクラスのパターンを指定する
* スキャンで検出できない動的クラスを強制的に CSS に含める
*/
/* ステータスカラー(bg と text の両方) */
@source inline("bg-{red,yellow,green,blue,purple}-{100,200,500,700}");
@source inline("text-{red,yellow,green,blue,purple}-{500,700,900}");
/* ボーダーカラー */
@source inline("border-{red,yellow,green,blue,purple}-{200,500}");
/* サイズバリアント(px / py / text) */
@source inline("px-{2,3,4,6,8} py-{1,2,3,4}");
@source inline("text-{sm,base,lg,xl,2xl}"); 実践例:ステータスバッジコンポーネント
API から取得したstatus 値に応じてバッジの色を変えるコンポーネントを例に、どこに何を書けばいいかを確認します。
@import "tailwindcss";
/* StatusBadge コンポーネントで使う全カラーパターンを強制出力 */
@source inline("bg-{green,yellow,red,blue,gray}-100");
@source inline("text-{green,yellow,red,blue,gray}-700");
@source inline("border-{green,yellow,red,blue,gray}-200");// API から渡ってくるステータス値
type Status = "success" | "warning" | "error" | "info" | "neutral";
// ステータスとカラー名のマッピング(完全なクラス名でなく色名だけ管理)
const statusColor: Record<Status, string> = {
success: "green",
warning: "yellow",
error: "red",
info: "blue",
neutral: "gray",
};
const statusLabel: Record<Status, string> = {
success: "完了",
warning: "注意",
error: "エラー",
info: "情報",
neutral: "未処理",
};
export function StatusBadge({ status }: { status: Status }) {
const c = statusColor[status];
// @source inline() で出力を強制しているので
// テンプレートリテラルで組み立てても大丈夫
return (
<span className={
`inline-flex items-center gap-1 rounded-full px-3 py-0.5 text-sm font-medium border
bg-${c}-100 text-${c}-700 border-${c}-200`
}>
{statusLabel[status]}
</span>
);
}🔍 Solution 01 と 02 の使い分け
可能な限り Solution 01(完全なクラス名を静的に書く)を優先 してください。@source inline() は生成クラス数が多くなるとCSS サイズが膨らみます。「どうしても動的に色名だけ持ちたい」「CMS から className が渡ってくる」などの避けられないケースに限って使うのが理想です。
Solution 03:@source でスキャン対象ファイルを追加する
クラス名が書かれたファイルが Tailwind の自動スキャン対象に含まれていない場合は、@source ディレクティブでスキャン対象を追加できます。
@import "tailwindcss";
/* node_modules 内のライブラリが Tailwind クラスを使っている場合 */
@source "../../node_modules/my-ui-library/dist";
/* .gitignore に入っているが、スキャンしたいディレクトリがある場合 */
@source "../scripts/class-allowlist.txt";
/* クラス名だけを列挙したホワイトリストファイルを用意する場合 */
@source "./tailwind-safelist.html";💡 スキャン対象追加ファイルの作り方
単純に「使いたいクラス名を列挙した HTML ファイル」を作ってスキャン対象に追加するのがシンプルな方法です。たとえばtailwind-safelist.html というファイルにbg-red-500 bg-green-500 bg-blue-500 ... と書いておき、@source "./tailwind-safelist.html"; で読み込むだけです。
v4 の @source 系ディレクティブ 全体整理
ここまで出てきた@source 関連のディレクティブを一覧で整理します。
@source inline("...")
ブレース展開パターンから生成される全クラスの CSS を強制出力する。ファイルを追加せずにクラスを確実に含める v4 の新機能。例:@source inline("bg-{red,green}-{100,500}");
@source "パス"
指定したファイルまたはディレクトリをスキャン対象に追加する。自動スキャンの対象外になっているファイルに使う。例:@source "../node_modules/some-lib";
@source not "パス"
指定したパスをスキャン対象から除外する。大きなディレクトリの一部だけを除外したいときに使う。例:@source not "./src/legacy";
どの解決策を使うか:判断フロー
実際の場面でどれを選べばよいか迷ったときは、このフローで判断してください。
🤔 解決策の判断フロー
動的クラスの問題が発生している
↓
クラスを組み立てているコードを自分で変更できる?
✓ Yes
Solution 01 を使う。完全なクラス名をオブジェクトで管理するように書き直す。TypeScript の型補完も活きて一石二鳥。
✗ No
外部ライブラリや CMS から渡ってくる場合。次の問いへ。
↓
使われうるクラスのパターンが予測できる?
✓ Yes
Solution 02 (@source inline() )を使う。ブレース展開でパターンを列挙して強制出力する。
✗ No
Solution 03 (@source "パス" )を使う。クラス名の一覧ファイルを作成してスキャン対象に追加する。
やってはいけないパターン:よくある間違い
❌ @source inline() に変数やテンプレートリテラルは使えない
/* @source inline() の引数は静的な文字列リテラルのみ */
@source inline($colors); /* NG: CSS 変数は使えない */
@source inline("bg-" + color); /* NG: 結合も使えない */
@source inline("bg-{red,green,blue}-500"); /* 静的な文字列のみ OK */ ❌ すべてのクラスを @source inline() に書こうとしない
⚠️ CSS サイズ爆発に注意
@source inline("bg-{すべての色}-{すべての濃淡}") のような書き方をすると、何百ものクラスの CSS が出力されて CSS ファイルサイズが大幅に増加します。 本当に動的に使われるクラスだけを、必要な範囲に絞って指定してください。
❌ 開発環境と本番環境で見え方が違う場合の勘違い
// なぜ開発環境では動くのか:
// Vite の開発サーバーは変更があるたびに差分スキャンを行う
// 動的クラスが「たまたま同じファイルのどこかに書かれていれば」拾われることがある
// しかし本番の完全ビルドでは厳密なスキャンが行われるためスタイルが消える
// よくあるケース:
// 開発中にテスト用で className="bg-red-500" と書いていたコードを消したとき
// → 開発中は残っていたが本番ビルドで初めて問題が顕在化する よくあるトラブルと解決策
❌ @source inline() を書いたのにクラスが反映されない
まずブレース展開の構文を確認してください。bg-{red,green}-500 と書く際、スペースを入れてはいけません(bg-{red, green}-500 は無効)。また、@source inline() は@import "tailwindcss"; の 後に 書く必要があります。開発サーバーを再起動して変更が反映されているかも確認してください。
❌ Solution 01 でオブジェクトに書いたのにスタイルが消える
オブジェクトの値が 完全なクラス名 になっているか確認してください。"bg-red" や"red-500" のように不完全な文字列では検出されません。"bg-red-500" のように Tailwind クラスとして成立する完全な文字列である必要があります。また、そのオブジェクトが定義されているファイルが Tailwind のスキャン対象に含まれているかも確認してください。
❌ v3 の safelist はどこに書けばいいのか
v4 にはtailwind.config.js がないため、v3 のsafelist オプションはそのままでは使えません。代替として@source inline() (動的パターンがある場合)か、クラス名を列挙した HTML ファイルを@source "パス" で追加する方法を使ってください。v3 から移行する場合、公式のnpx @tailwindcss/upgrade コマンドが一部のsafelist 設定を自動変換してくれます。
❌ ライブラリのコンポーネントに Tailwind クラスを渡したらスタイルが当たらない
外部ライブラリのソースは Tailwind のスキャン対象外のため、 呼び出し側のコードに完全なクラス名が書かれていても、そのライブラリのコンポーネント内部に渡された時点では検出されない ことがあります。呼び出し元のファイルにクラス名が静的に書かれていれば通常は検出されます。それでも問題がある場合は@source inline() で対象クラスを明示的に追加してください。
第5回のまとめ
今回学んだこと
- Tailwind はソースファイルを 静的テキストとしてスキャン し、見つかったクラス名のCSSだけを出力する。JavaScript を実行して動的に計算された結果は検出できない
- テンプレートリテラルで文字列を結合してクラス名を組み立てると(例:
`bg-${color}-500`)、ビルド後にスタイルが消える - 最優先の解決策は Solution 01 :完全なクラス名を値にもつオブジェクトで管理する。コードの可読性・型安全性も向上する
@source inline("bg-{red,green}-{100,500}")を CSS に書くと、ブレース展開で生成されるすべてのクラスが強制出力される(v4 の新機能)@source "パス"でスキャン対象ディレクトリ・ファイルを追加できる。外部ライブラリや .gitignore 対象ファイルに使う@source inline()の使いすぎは CSS サイズ増加につながる。本当に必要な範囲だけに絞ること
📌 第5回 まとめ
「スタイルが消える」というトラブルの原因と解決策を根本から理解できました。いよいよ次回は最終回です。これまで学んだすべての知識を組み合わせて、実際のプロジェクトで使えるコンポーネント設計パターンを整理します。再利用性が高く保守しやすいコンポーネントの作り方を、具体的なコードで解説します。
📝 ▶ 次回(第5回)
次は「 」です。
