フロントエンド・API連携

Tailwind CSSの動的クラス生成:clsxとtailwind-mergeによるスタイル崩れ防止

Tailwind CSSでボタンの色やカードの余白を動的に変えようとして、className にクラス名を文字列結合した。コード上は間違っていない。ブラウザのHTMLを見ても、それらしいクラス名が付いている。それなのに色が変わらない。…

Tailwind CSSの動的クラス生成:clsxとtailwind-mergeによるスタイル崩れ防止

この現象は、Tailwind CSSの機嫌が悪いというより、ビルド時の仕組みと動的クラス生成の相性が悪いことで起きる。さらに、クラス名が正しく出力された後にも、w-20w-10 のような競合が残る。文字列の後ろに書いたクラスが勝つと思っていると、ここでも静かに足元をすくわれる。

Tailwind CSSの動的クラス結合では、少なくとも二つの問題を分けて考える必要がある。

  • そもそも必要なCSSがビルドで生成されているか
  • 生成されたクラス同士の競合をどう解決するか

clsxtailwind-merge は、この二つのうち主に後者を整理するための道具だ。ただし、何でも解決する魔法の関数ではない。ここを勘違いすると、cn ヘルパーを導入したのに bg-${color}-500 が効かない、という別の罠に移動するだけである。

Tailwind CSSの動的クラスが効かない理由

Tailwind CSSは実行時ではなく、ビルド時にクラスを探す

Tailwind CSSのユーティリティクラスは、ブラウザでJavaScriptが実行された後に生成されるわけではない。ビルド時にソースコードを静的解析し、そこに登場するクラス名を抽出してCSSを出力する。

たとえば、ソースコードに bg-red-500 と書かれていれば、Tailwind CSSはそのクラスを見つけて必要なCSSを生成できる。text-smmd:grid-cols-2 のようなクラスも同じだ。

一方、次のような書き方には問題がある。

bg-${color}-500

colorred が入れば、実行時には bg-red-500 になる。しかし、ビルド時のTailwind CSSから見ると、ソースコードに完全な bg-red-500 が存在しているわけではない。見えているのは bg-${color}-500 という分解された文字列だ。

そのため、実行時にHTMLへクラス名が付与されても、対応するCSSが出力されていない。クラス名はある。CSSもありそうに見える。だが、実際にはない。この状態が、いわゆる「Tailwindの動的クラスが効かない」問題の代表例である。

cn はクラス名を整える道具であって、Tailwind CSSに存在しないクラスを生成させる道具ではない。

文字列補間をやめて、完全なクラス名を静的に書く

動的に色を変えたい場合でも、クラス名の断片を組み立てるのではなく、完成したクラス名をコード上に残す。たとえば、色とクラス名の対応表を用意し、red のときは bg-red-500blue のときは bg-blue-500 を返す形にする。

この方法なら、Tailwind CSSはソースコードから完全なクラス名を見つけられる。

Reactであれば、状態に応じて次のような対応を作るイメージだ。

  • primarybg-blue-600 hover:bg-blue-700 text-white
  • dangerbg-red-600 hover:bg-red-700 text-white
  • secondarybg-gray-100 hover:bg-gray-200 text-gray-900

ここで大事なのは、値の組み合わせを実行時に生成しないことだ。bg-${variant}-600 のように書くのではなく、variant ごとに静的なクラス文字列を定義する。

これは少々面倒に見える。個人開発では、もっと短く書きたいという気持ちもよく分かる。しかし、ビルドツールにクラスを認識させるために、人間側が少しだけ明示的に書く。結局のところ、この妥協が一番安定する。

clsx を使っても、動的クラスの抽出問題は消えない

clsx は条件付きでクラス名を結合するためのライブラリだ。真偽値、オブジェクト、配列などを受け取り、不要な値を除外しながらクラス文字列をまとめてくれる。

たとえば、ボタンが無効状態のときだけ opacity-50cursor-not-allowed を加える、といった処理には向いている。disabled が偽なら、その条件に紐づくクラスは出力されない。undefinednull のような値も自然に除外される。

ただし、clsx がやっているのは、あくまでクラス文字列の結合と整形だ。Tailwind CSSのビルド処理を理解して、足りないCSSを生成してくれるわけではない。

つまり、次の二つは別問題である。

問題主な原因対応
クラス名を付けたのにスタイルがないTailwind CSSがクラスを抽出できていない完全なクラス名を静的に記述する
クラス名はあるが意図した方が勝たない同じプロパティのクラスが競合しているtailwind-merge で競合を解決する
条件によって余計なクラスまで付く条件分岐の組み立てが複雑clsx で条件付き結合を整理する

この切り分けをしないと、「clsx を入れたのに動かない」「tailwind-merge を入れたのに色が表示されない」という話になる。ライブラリが悪いのではなく、担当している問題が違う。

clsx の使い方と条件付きクラス

手書きの文字列結合が壊れやすい理由

クラス名を文字列で直接結合する方法は、最初の数行なら問題ない。

"rounded px-4 py-2 " + (isActive ? "bg-blue-600" : "bg-gray-200")

ただ、状態が増えると急速に読みにくくなる。無効状態、サイズ、テーマ、アイコンの有無、レスポンシブ表示といった条件が積み重なると、空白の扱いや条件分岐の抜けが発生しやすい。

テンプレートリテラルを使っても、根本的な問題は変わらない。

${baseClass} ${isActive ? activeClass : ""} ${disabled ? disabledClass : ""}

クラスが増えるほど、どこに何が付くのかを読むために目が滑る。しかも、条件が偽のときに空文字や undefined が混ざり、最終的な文字列に不要な空白が残ることもある。ブラウザはたいてい許容してくれるが、許容されることと、管理しやすいことは別だ。

clsx では、条件に応じたクラスをオブジェクト形式で表現できる。たとえば、isActive が真のときだけ bg-blue-600 を付けるなら、clsx({ "bg-blue-600": isActive }) という形にできる。無効状態なら clsx({ "opacity-50 cursor-not-allowed": disabled }) と書ける。

この書き方の良さは、クラスと条件の対応が近いことにある。条件式の中にクラスを埋め込み、さらに文字列を連結するより、何がいつ付くのかが見えやすい。

配列や複数条件にも対応できる

実際のコンポーネントでは、固定クラス、サイズ別クラス、状態別クラス、利用側から渡された追加クラスをまとめることになる。

たとえばボタンなら、次のような責務に分けられる。

1. すべてのボタンに共通するクラスを付ける

2. size に応じてパディングや文字サイズを切り替える

3. variant に応じて背景色や文字色を切り替える

4. disabled のときだけ操作不能らしい見た目を追加する

5. 外から渡された className を最後に反映する

この構造を配列として clsx に渡すと、各条件を独立して管理できる。配列の中に条件式やオブジェクトを混ぜられるため、複雑なUIでもある程度は見通しを保てる。

ただし、ここで一つ現場的な注意がある。clsx はクラス名を読みやすく結合するが、Tailwind CSSの競合は解決しない。

clsx("w-20", "w-10") の結果は、基本的に w-20 w-10 というクラス文字列になる。どちらを採用するかを判断して、不要な方を削ってくれるわけではない。

この挙動は、一般的なCSSの感覚と少しずれる。HTMLの属性値では後から書いたものが勝つように見えるが、実際のCSSでは、スタイルシート内の定義順やセレクタの条件などが適用結果に影響する。Tailwind CSSのユーティリティクラスを並べた順番が、そのまま優先順位になるとは限らない。

tailwind-merge が解決するクラス競合

w-20 と w-10 を同時に渡すと何が起きるか

コンポーネントを再利用していると、内部で基本スタイルを定義しつつ、利用側から見た目を上書きしたくなる。

内部のボタンに px-4 py-2 text-sm を設定している。ところが、ある画面だけ px-8 にしたい。そこで利用側から className="px-8" を渡す。

最終的なクラス文字列が px-4 py-2 text-sm px-8 になれば、直感的には px-8 が勝ってほしい。しかし、CSSの定義順によっては、HTML上で後に書かれた px-8 が確実に勝つとは限らない。

ここで使うのが tailwind-merge だ。twMerge は、同じCSSプロパティに影響するTailwind CSSのクラスを検出し、競合するクラスを整理する。後から渡されたクラスを優先し、先に渡された同種のクラスを取り除く。

twMerge("w-20", "w-10") であれば、最終的に幅の指定としては後の w-10 を残す、という考え方になる。px-4px-8text-smtext-lgbg-blue-500bg-red-500 のような組み合わせでも、同じグループの競合を処理できる。

このライブラリの価値は、クラス文字列を短くすることだけではない。コンポーネントの標準スタイルと、利用側からの上書き指定を共存させやすくなる点にある。

clsx と tailwind-merge の役割を分ける

二つのライブラリは似た場所で使うが、処理の役割は違う。

  • clsx は条件分岐を含むクラス名の結合
  • tailwind-merge はTailwind CSSクラス同士の競合解決

この二つを順番に組み合わせるのが、よく使われる cn ヘルパーだ。

TypeScriptで実装する場合、clsx から ClassValue 型を読み込み、可変長引数を受け取って clsx の結果を twMerge に渡す。実装の形としては、cn(...inputs) の内部で twMerge(clsx(inputs)) を返すものになる。

この順序にも意味がある。まず clsx で条件分岐や配列を一つのクラス文字列に整え、その結果を tailwind-merge に渡して競合を処理する。

逆に、条件付きの構造を整理しないまま twMerge だけを呼び出しても、コード全体の読みやすさはそれほど改善しない。もちろん単純な文字列なら動くが、コンポーネントが成長したときに再び手作業の連結へ戻ることになる。

clsx は条件を片付け、tailwind-merge は衝突を片付ける。似た仕事に見えて、現場では担当が違う。

cn ヘルパーでコンポーネントの上書きを安定させる

標準スタイルと利用側の指定を一つにまとめる

再利用可能なUIコンポーネントでは、内部の標準スタイルと外部の追加クラスを一つにまとめる場面が多い。

たとえば、ボタンの内部に次のような固定スタイルがあるとする。

  • inline-flex
  • items-center
  • justify-center
  • rounded-md
  • font-medium
  • transition-colors

さらに、サイズによって h-9 px-3 text-smh-11 px-5 text-base を切り替える。バリエーションによって背景色と文字色を変える。そして最後に、呼び出し元が渡した className を反映する。

このとき、単純な文字列結合だけで書くと、呼び出し元が指定したクラスが内部クラスに負ける可能性がある。cn を通せば、条件付きクラスを結合した上で、競合するユーティリティを整理できる。

実装の考え方は次の通りだ。

1. 共通クラスを最初に渡す

2. サイズやバリエーションのクラスを条件付きで渡す

3. disabled など状態に応じたクラスを渡す

4. 呼び出し元の className を最後に渡す

5. clsxtwMerge を通した結果を className に設定する

最後に外部クラスを渡すのは、上書きの意図をコード上でも明確にするためだ。tailwind-merge は後から渡されたクラスを優先するため、コンポーネントの標準値を利用側で変更しやすい。

ただし、何でも利用側から上書きできる設計が良いわけではない。高さ、余白、色、角丸などをすべて自由にすると、コンポーネントの責務が曖昧になる。自由度を上げた結果、画面ごとに別の見た目になり、共通コンポーネントの意味が消えるという罠がある。

バリアント管理と cn は別の話

ボタンの種類が増えると、clsx の条件分岐だけでなく、バリアント管理の仕組みも欲しくなる。

たとえば次のような組み合わせだ。

  • 色: primary、secondary、danger
  • サイズ: small、medium、large
  • 表示状態: 通常、ホバー、無効、ローディング
  • 幅: 内容に合わせる、親要素いっぱい

この場合、variantsize の値からクラスを選ぶ設計にする。クラスの対応表は静的に記述し、選択された結果を cn に渡す。

ここで、cn がバリアント定義まで担当する必要はない。cn はあくまでクラス文字列の結合と競合解決を担う薄い関数にしておく方が扱いやすい。

バリアントの組み合わせを一箇所にまとめるのか、コンポーネント内に書くのかは、プロジェクトの規模で決めればいい。小さな個人開発で大掛かりな抽象化を入れると、ボタンを修正するために三つの定義ファイルを行き来することになる。これはこれで別の理不尽な仕様だ。

次のような基準なら、判断しやすい。

  • 一つのコンポーネントに状態が少ないなら、cn と単純な対応表で十分
  • 複数コンポーネントで同じバリアント設計を共有するなら、定義を共通化する
  • クラスの組み合わせに依存関係があるなら、組み合わせを明示する
  • 外部からの上書きを許可する範囲は、コンポーネントごとに決める

tailwind-merge がすべての独自クラスを理解するとは限らない

tailwind-merge はTailwind CSSの標準的なユーティリティクラスを前提に競合を判断する。プロジェクト独自のクラスや、カスタムプラグインで追加したクラスについては、期待どおりに同じグループとして認識されない場合がある。

たとえば、独自プラグインで作ったクラスが同じCSSプロパティを変更していても、tailwind-merge がその関係を知らなければ、どちらを残すべきか判断できない可能性がある。

この場合、次のような対応が現実的だ。

  • 独自クラスとTailwindユーティリティを同じ箇所で混在させすぎない
  • 独自クラスに役割を持たせ、上書き可能な値はユーティリティ側へ寄せる
  • tailwind-merge の設定を拡張できる構成にする
  • 重要な表示だけはブラウザで最終結果を確認する

cn を使っているから競合は全部消える」と考えると、独自クラスを追加した時点でまた静かな破綻が始まる。ライブラリが賢いほど、対応範囲の外側で起きる問題を見落としやすい。

ReactとVueでの動的クラス運用

Reactでは className の流れを一本化する

ReactでTailwind CSSを使う場合、className はコンポーネントの外部APIになりやすい。内部のクラスと、利用側から渡された className をどこで結合するかを毎回変えると、コンポーネントごとに挙動が違ってくる。

共通の cn を用意しておけば、少なくとも結合のルールを揃えられる。

たとえばカードコンポーネントなら、基本のクラスとして rounded-lg border bg-white shadow-sm を持たせ、利用側から shadow-mdbg-gray-50 を渡す。cn を通していれば、同じプロパティを変更するクラスは後から指定したものを優先できる。

ただし、Reactのpropsをそのままクラス名の断片として受け取る設計は避けたい。color="red" を受け取り、そこから bg-${color}-500 を作る方法は、Tailwind CSSの静的抽出に引っかかる。

代わりに、propsの値を静的なクラス文字列へ対応付ける。

  • color="red" なら bg-red-500
  • color="green" なら bg-green-500
  • color="blue" なら bg-blue-500

この対応表は、Reactの条件分岐でも、オブジェクトでも構わない。要は、Tailwind CSSが完全なクラス名をソースコードから見つけられる状態にすることだ。

Vueでは :class の配列とオブジェクトを整理する

Vueの :class は、配列やオブジェクトによる条件付きクラスと相性が良い。is-active のような状態クラスは、オブジェクト形式で管理しやすい。

一方で、クラスの競合という問題はReactと変わらない。テンプレート側で px-4 を指定し、コンポーネントのcomputed側で px-8 を追加したからといって、期待どおりに後者が勝つとは限らない。

Vueでも、最終的に生成するクラス文字列を clsxtailwind-merge で整理する設計は使える。ただし、Vueのテンプレート構文、computed、propsのデフォルト値がそれぞれ別の場所に散らばると、最終的なクラスの組み立てが追いにくくなる。

Vueでよくあるのは、見た目の状態をcomputedに集約し、テンプレートには完成したクラス文字列を渡す方法だ。statussize の値から静的なクラスセットを選び、外部からの追加クラスを最後にマージする。

ReactでもVueでも、フレームワーク固有の書き方より先に、次の責務を分けると事故が減る。

1. 状態の値を決める

2. 状態に対応する完全なクラス名を選ぶ

3. 条件付きクラスを結合する

4. 競合するクラスを整理する

5. 最終的な属性へ渡す

この順番を一箇所で確認できるようにする。フレームワークを変えても、考え方はほとんど同じだ。

動的クラス生成で起きる、もう一段深い問題

tailwind-merge はCSSのすべてを解析するわけではない

tailwind-merge はTailwind CSSのクラス同士の競合を判断するが、ブラウザのCSSエンジンそのものではない。すべてのセレクタの詳細な優先順位を計算するものでもない。

標準的なユーティリティ同士の競合であれば、かなり便利に扱える。しかし、複雑なセレクタ、独自のバリアント、プラグイン由来のクラス、CSS Modulesや通常のCSSとの組み合わせになると、挙動の確認が必要になる。

たとえば、Tailwind CSSのユーティリティと、別ファイルのCSSで同じプロパティを指定している場合、tailwind-merge が処理できるのは基本的にクラス文字列内のTailwindクラスだ。外部CSSの読み込み順やセレクタの詳細度まで解決してくれるわけではない。

したがって、問題を次の三層に分けるとよい。

  • クラス名がHTMLへ出力されているか
  • 必要なTailwind CSSがビルド成果物に含まれているか
  • ブラウザのCSS適用結果として何が勝っているか

cn の結果だけを見て、画面表示の問題が解決したと判断するのは早い。最終的には開発者ツールで、要素にクラスが付いているか、該当するCSSルールが存在するか、どのルールが打ち消されているかを見る。

レスポンシブや状態バリアントでは意図を明示する

md:px-8px-4 のように、画面幅の条件が異なるクラスは、単純な同一プロパティの競合とは扱いが違う。hover:bg-blue-700focus:ring-2 も、通常状態のクラスと同じグループとして乱暴に削除できるものではない。

tailwind-merge はバリアントを考慮したマージを行うが、クラスの指定意図まで補ってくれるわけではない。次のような指定では、何を通常状態にし、何を特定条件にするのかをコード上で明確にした方がいい。

  • 通常時の背景色と、ホバー時の背景色
  • 通常時の余白と、画面幅が変わったときの余白
  • フォーカス時のアウトラインと、無効状態の見た目
  • ダークモード時の背景色と、テーマ別の背景色

特に、条件付きクラスを複数の関数やcomputedに分散させると、意図しない上書きが起きやすい。後から来たクラスが勝つというルールを採用するなら、呼び出し順そのものを設計として扱う必要がある。

動的な任意値はクラス名の生成方法を慎重に選ぶ

Tailwind CSSには任意値を使う書き方がある。幅を固定のクラスではなく、特定の値で指定したいケースだ。

ただし、任意値を文字列補間で作る場合も、Tailwind CSSの静的抽出が問題になる可能性がある。値が完全なクラス名としてソースコードに登場しないなら、ビルド時にCSSが生成されない。

動的な値をどうしても実行時に変えたい場合、すべてをTailwind CSSのクラスで表現しようとしない方がよい場合もある。

たとえば、ユーザーが自由に選んだ色や、APIから取得した数値をそのまま反映する場合は、CSSカスタムプロパティやインラインスタイルの方が適していることがある。Tailwind CSSで静的に管理するのは、あらかじめ定義できる状態やバリエーションに限定する。

この線引きは、Tailwind CSSを否定する話ではない。むしろ、Tailwind CSSに向いている仕事だけを任せるという話だ。

  • デザインシステムで決めた色や余白はTailwind CSS
  • APIから変動する実データはCSS変数やインラインスタイル
  • 状態の種類が固定されている場合は静的な対応表
  • ユーザー入力の値をそのままクラス名にする処理は避ける

力技で全てをユーティリティクラスへ押し込むと、ビルド設定と表示仕様の両方が複雑になる。

実装時に確認しておきたい設計ポイント

動的クラスの問題は、ライブラリを追加すれば終わるものではない。コンポーネントの設計とビルド設定が噛み合っているかを見る必要がある。

まず確認する順番

スタイルが崩れたとき、いきなり tailwind-merge の設定を疑う前に、次の順番で見ると切り分けやすい。

1. ブラウザの要素に、期待するクラス名が付いているか確認する

クラス名自体がなければ、条件分岐やpropsの受け渡しに問題がある。

2. クラス名がソースコードに完全な形で存在しているか確認する

bg-${color}-500 のような分割された文字列なら、Tailwind CSSの抽出漏れを疑う。

3. ビルド後のCSSに該当するユーティリティが含まれているか確認する

HTMLにクラスがあっても、CSSがなければ当然ながら見た目は変わらない。

4. 同じプロパティのクラスが複数付いていないか確認する

p-4 p-8text-sm text-lgbg-white bg-gray-900 のような競合を探す。

5. cn を通した後の文字列を確認する

tailwind-merge が期待どおりに不要なクラスを取り除いているかを見る。

6. Tailwind CSS以外のCSSが上書きしていないか確認する

セレクタの詳細度、読み込み順、インラインスタイルも含めて確認する。

この順序なら、「CSSが存在しない問題」と「存在するが負けている問題」を混同しにくい。

cn の置き場所を決める

cn は多くのコンポーネントから使うため、共通のユーティリティファイルに置くのが一般的だ。Reactなら libutils 配下、Vueでもプロジェクトの共通ユーティリティとして管理することが多い。

ただし、あらゆるクラス結合を必ず cn に通すべきだとは限らない。静的なクラスが一つだけの要素まで cn を経由させると、かえってコードの意味が薄くなる。

使い分けとしては、次の程度でよい。

  • クラスに条件分岐があるなら clsx を使う
  • 外部からクラスを受け取り、標準スタイルを上書きするなら tailwind-merge を使う
  • その両方がある再利用コンポーネントでは cn を使う
  • 静的な装飾だけなら通常のクラス文字列で済ませる

抽象化は、繰り返しが見えてからでいい。最初から全てを汎用化すると、見た目の修正に必要なコード量だけが増えるという罠がある。

クラスの順番をチームのルールにする

tailwind-merge を導入しても、クラスの順番が無意味になるわけではない。むしろ、「どの値を標準とし、どの値を上書き可能にするか」をコードで表すために、順番は重要になる。

基本のクラス、バリアント、状態、外部クラスという順番にしておくと、読む側も意図を追いやすい。

たとえば、次のような構造だ。

  • 共通レイアウト
  • サイズ
  • 色やテーマ
  • 状態
  • 呼び出し元から渡された追加クラス

この流れを崩さなければ、後から className を追加した人も、どこで上書きされるのかを想像しやすい。コード規約として大げさに文書化しなくても、共通コンポーネントの書き方を揃えるだけで効果はある。

clsx と tailwind-merge を使わない方がよいケース

二つのライブラリは便利だが、導入すればコードが必ず良くなるわけではない。

クラスの種類が少なく、条件も一つしかないなら、通常の条件式で十分な場合がある。依存パッケージを増やしてまで解決する問題ではない。

また、クラス名をデザイン設定の中心に据えていない部分では、CSS変数や専用のスタイル定義の方が分かりやすいこともある。グラフの色、ユーザーが選択するテーマカラー、サーバーから取得した幅などは、Tailwind CSSの静的クラスだけで表現しようとすると無理が出る。

さらに、コンポーネントのAPIが複雑になりすぎている場合、cn でその複雑さを隠してはいけない。variantsizetonedensitystate が増えた結果、クラスを合成するだけで何十通りもの状態が生まれているなら、問題はクラス結合ではなく設計にある。

その場合は、次のように整理する方がよい。

  • 状態の種類を減らす
  • 似たバリアントを統合する
  • 見た目の責務をコンポーネントから分離する
  • どうしても必要な組み合わせだけを明示する
  • 外部から自由に上書きできる範囲を狭める

「何でも className で変更できる」は、一見すると柔軟だが、運用では仕様の抜け道になりやすい。半年後に同じコンポーネントを触る自分が、利用側の上書きまで追える設計かどうか。それが実務上の判断基準になる。

結局、コードで解決する部分と運用で守る部分

Tailwind CSSの動的クラス結合で、clsxtailwind-merge はかなり頼りになる。条件分岐を読みやすくし、同じプロパティのクラス競合を整理し、共通コンポーネントの上書きも扱いやすくしてくれる。

一方で、Tailwind CSSのビルド時抽出という前提は変わらない。bg-${color}-500 のようなクラス名を実行時に組み立てても、cn がそれを救ってくれるわけではない。必要なクラス名を静的に書くか、動的な値をCSS変数や別の仕組みへ逃がす必要がある。

実装の基本形はシンプルだ。

  • 完全なTailwind CSSクラスをソースコードに残す
  • 条件付きクラスの結合に clsx を使う
  • 競合するクラスの整理に tailwind-merge を使う
  • 両方をまとめた cn を再利用コンポーネントで使う
  • 外部から渡すクラスは最後にマージする
  • 独自クラスや外部CSSまで自動解決できると思わない

このあたりを守れば、ReactでもVueでも、動的なUIのスタイル崩れはかなり減る。

ただし、全てをコードで解決する必要はない。バリアントの数を増やさない、クラス名の命名規則を揃える、外部からの上書きを限定する、といった運用上の判断も同じくらい効く。

理想的な設計では、どんなクラスでも自由に組み替えられるのかもしれない。だが、炎上する案件で最後に必要になるのは、理想よりも「このコンポーネントはここまで変更できる」という境界線だ。

Tailwind CSSの動的クラス結合は、短いコードを書くためだけの話ではない。ビルド時に存在するクラス、実行時に選ばれるクラス、最終的に勝つクラスを分けて管理するための話である。clsxtailwind-merge は、その境界をコードへ落とし込むための、地味だが実戦的な道具だ。運用で守る範囲を決めた上で、面倒な競合だけをコードに片付けさせる。それくらいの距離感が、個人開発では一番長持ちする。

よくある質問

Tailwind CSSの動的クラスが効かないのはなぜですか?
Tailwind CSSはビルド時にソースコードを静的解析してクラスを抽出します。`bg-${color}-500`のようにクラス名を実行時に組み立てると、対応するCSSが生成されないことがあります。
clsxを使えばTailwind CSSの動的クラス問題は解決できますか?
解決できません。`clsx`は条件付きクラスの結合や不要な値の除外を行うライブラリであり、Tailwind CSSに不足しているCSSを生成する機能はありません。
tailwind-mergeは何を解決しますか?
`tailwind-merge`は、`w-20`と`w-10`、`px-4`と`px-8`のように同じCSSプロパティへ影響するTailwind CSSクラスの競合を整理します。後から渡された同種のクラスを優先し、先に渡されたクラスを取り除きます。
clsxとtailwind-mergeはどの順番で使いますか?
まず`clsx`で条件分岐や配列を含むクラスを一つの文字列にまとめ、その結果を`tailwind-merge`に渡して競合を処理します。この処理をまとめた関数が`cn`ヘルパーです。
Tailwind CSSで自由に変わる色や幅を扱うにはどうすればよいですか?
あらかじめ定義できる状態やバリエーションは、完全なクラス名の静的な対応表で管理します。ユーザーが選んだ色やAPIから取得した数値など、実行時に自由に変わる値にはCSSカスタムプロパティやインラインスタイルが適する場合があります。

参考情報