はじめに
既存のReact + ViteプロジェクトにTailwind CSS v4を段階的に導入している際、<Link>コンポーネントにtext-blue-600のようなTailwindのユーティリティクラスを付けたのに、色が一切反映されないという現象に遭遇しました。詳細度で考えればクラス指定が要素セレクタに負けるはずはないのに、なぜか負けている——原因はCSSカスケードレイヤーという仕様でした。本記事ではこの現象と原因、対処法をまとめます。
問題
構成は以下の通りです。Tailwind本体はtailwind.cssに分離し、既存の生CSSはindex.cssにそのまま残しています。preflight(全要素グローバルリセット)はTailwind未導入の他画面への影響を避けるため導入していません。
/* tailwind.css */
@import "tailwindcss/theme" layer(theme);
@import "tailwindcss/utilities" layer(utilities);
/* index.css(既存の生CSS) */
a {
color: red;
}
<Link
to="/articles"
className="text-blue-600 ..."
>
<Link>(<a>タグ)にtext-blue-600を付けて青文字にしたつもりが、見た目上は常にindex.cssのa { color: red; }が採用され、なぜかredになってしまい、想定と違う結果になりました。
詳細度だけで考えれば
a → 0-0-1(要素セレクタ)
.text-blue-600 → 0-1-0(クラスセレクタ)
クラスセレクタの方が強いはずです。しかし実際には要素セレクタの指定が勝ち続けていました。
解決方法
原因はCSSカスケードレイヤー
CSSにはカスケードレイヤー(@layer)という仕様があり、複数のCSSルールが競合したときの優先順位を、詳細度より上の段階で先に決定してしまう仕組みがあります。
優先順位は次の順で決まります。
1. 非レイヤー(@layerで囲まれていない、素のCSS) ← 最強
2. 名前付きレイヤー(後から初登場したレイヤーほど強い)
3. UAデフォルトスタイル
重要なのは、この段階の勝敗が決まった時点で、詳細度の比較まで話が進まないという点です。
今回のケースでは:
-
tailwind.cssが@import "tailwindcss/utilities" layer(utilities);でTailwindのクラス群をすべて**utilitiesという名前付きレイヤー**に格納している - 一方、
index.cssのa { color: red; }は@layerで囲まれていない非レイヤーのまま
非レイヤーは名前付きレイヤーに常に勝つという仕様があるため、詳細度では.text-blue-600の方が強いにもかかわらず、レイヤーの段階ですでにaが勝ってしまっていました。
なぜこうなったか
Tailwindを部分導入する際、新しく追加する側(Tailwind)だけが@layerの恩恵を受け、既存の生CSS側は@layerを使わずそのまま残っていました。この非対称性(片方だけレイヤー化されている状態)が、直感に反する優先順位を生んだ原因です。
対処法:レイヤーの並び順を先に確定させる
@layerは、中身を書かずに名前の並び順だけを宣言することができます。これを使って、意図した優先順位を明示的に確定させます。
/* tailwind.css の一番先頭 */
@layer theme, base, utilities;
@import "tailwindcss/theme" layer(theme);
@import "tailwindcss/utilities" layer(utilities);
/* index.css */
@layer base {
a {
color: red;
}
}
ここでのポイントは2つです。
- 既存の生CSSルールを
@layer base {}で囲み、名前付きレイヤーに参加させる -
加えて、ファイルの冒頭で
@layer theme, base, utilities;と並び順だけを先に宣言する
②が抜けていると、baseという名前が実際に登場する順番(utilitiesより後)によってbaseの方が強いレイヤーになってしまい、意図と逆の結果になります。中身のない並び順宣言を先頭に置くことで、その後どこにどんな順で中身が書かれても、確定した並び順が維持されます。
全部レイヤーに入れるべきとは限らない
既存の生CSSルールをすべて@layer baseに入れる必要はありません。判断基準は、Tailwindのユーティリティクラスと同じプロパティを取り合っているかどうかです。
| ルール | 対応 |
|---|---|
a { color: ...; } など、Tailwindクラスと同じプロパティが競合する要素セレクタ |
@layer baseに入れて競合を解消する |
* { box-sizing: border-box; } など、全要素向けのリセットでTailwindクラスと衝突する見込みがないもの |
非レイヤーのままでいい(むしろ非レイヤーの方が、Tailwind未導入の他画面にも確実に効く) |
box-sizingのようにTailwind側に対応する明示的なユーティリティクラス(box-borderなど)を意図的に使わない限り競合しない場合は、非レイヤーのままにしておく方が、Tailwindを使っていない画面への影響を保ちやすくなります。
おわりに
Tailwindを既存プロジェクトへ段階的に導入する際、「新しく入れる方だけが@layerのグループに入り、既存の生CSSはレイヤーの外に取り残される」という非対称な状態は起きやすい構図だと感じました。CSSカスケードレイヤーは「非レイヤーが名前付きレイヤーに常に勝つ」という、詳細度とは独立したルールを持っているため、これを知らないと「詳細度で勝っているはずのクラスが効かない」という現象の原因究明に時間がかかります。
移行を段階的に進める場合は、既存の生CSSのうちTailwindクラスと競合する可能性のあるセレクタを洗い出し、@layer baseに入れた上で、レイヤーの並び順を明示的に先頭で宣言しておくことをおすすめします。
参考
JISOUのメンバー募集中!
プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
日本一のアウトプットコミュニティでキャリアアップしませんか?
興味のある方は、ぜひホームページをのぞいてみてください!
▼▼▼
https://projisou.jp