CSS を書いていて「なぜかスタイルが上書きされない」「打ち消すために !important を付けてしまった」という経験はないでしょうか。原因の多くは、セレクターの詳細度(specificity)が競り上がってしまうことにあります。@layer(カスケードレイヤー)は、スタイルをいくつかの「層」に分け、層の順番で勝ち負けを決める仕組みです。詳細度の勝負より先に層の順番で決まるため、リセット CSS・ライブラリ・自作スタイルの関係を意識的に整理できます。この記事では基本の書き方から、レイヤーに入れないスタイルの扱い、@import との組み合わせ、revert-layer までを動かせるデモ付きで解説します。
目次
@layer はスタイルの強さを「層の順番」で決める
ブラウザーは、同じ要素に複数のスタイルが当たったとき、いくつかの基準を順番に見て「どれを採用するか」を決めます。これがカスケード(cascade)です。従来はまず !important の有無を見て、次にセレクターの詳細度、それも同じなら記述順、という流れでした。@layer はここに詳細度より前に評価される段階を追加します。
つまり、あるルールとあるルールが別々のレイヤーに属していれば、セレクターがどれだけ複雑でも、あとに宣言されたレイヤーのほうが勝ちます。ID セレクターで書かれたライブラリのスタイルを、クラス1つの自作スタイルで上書きする、といったことが素直にできるわけです。詳細度は「同じレイヤーの中で複数のルールが競合したとき」にだけ効いてきます。
基本の書き方|@layer でルールを層に入れる
もっとも単純な使い方は、レイヤー名を付けたブロックの中に通常の CSS を書く形です。
/* reset レイヤーにルールを入れる */
@layer reset {
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
}
/* components レイヤーにルールを入れる */
@layer components {
.card {
padding: 16px;
border-radius: 8px;
}
}
同じ名前で @layer を何度書いても構いません。同名のブロックは1つのレイヤーとして扱われ、中身が結合されます。ファイルを分けて書いても、名前さえ合っていれば同じ層にまとまるということです。
レイヤーの順番は、その名前が最初に登場した順で決まります。上の例では reset → components の順に並ぶので、両方に同じ要素へのルールがあれば components が勝ちます。あとから書いたファイルで先に components が登場してしまうと順番が入れ替わってしまうため、実務では次に説明する「先に順番だけ宣言する」書き方を使います。
レイヤーの順番は最初にまとめて宣言する
中身を持たない @layer をカンマ区切りで書くと、レイヤー名とその順序だけを先に登録できます。CSS の先頭にこれを置いておけば、以降どんな順でルールを書いても優先順位は宣言どおりに保たれます。
/* 左から順に弱く、右にいくほど強い */
@layer reset, base, components, utilities;
/* 以降は書く順番を気にしなくてよい */
@layer utilities {
.text-center { text-align: center; }
}
@layer base {
body { font-family: sans-serif; }
}
下のデモで、順序の宣言が詳細度より優先されることを確認してみてください。components レイヤーのルールは button という要素セレクター、base レイヤーのルールは .btn というクラスセレクターで、詳細度だけを見ればクラスのほうが強いはずです。それでもボタンが青いのは、components があとのレイヤーだからです。1行目を @layer components, base; に書き換えると、灰色のボタンに変わります。
<button class="btn">レイヤーで決まるボタン</button>
/* レイヤーの優先順位をここで宣言する(あとに書いたレイヤーほど強い) */
@layer base, components;
/* components レイヤー:詳細度は 0,0,1(要素セレクター1つ)と低い */
@layer components {
button {
background: #2563eb;
color: #fff;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-size: 16px;
}
}
/* base レイヤー:詳細度は 0,1,0(クラス)で本来はこちらが強いが……
base は components より前のレイヤーなので負ける */
@layer base {
.btn {
background: #d1d5db;
color: #111827;
}
}
レイヤーに入れていないスタイルが一番強い
@layer を導入するときに必ず押さえておきたいのが、どのレイヤーにも属していないスタイル(unlayered styles)は、すべてのレイヤーより強いというルールです。レイヤーは「詳細度の勝負に入る前の段階」で並び、レイヤー外のルールはその列のいちばん後ろ、つまり最強の位置に置かれます。
これは直感に反しやすいのですが、既存サイトへ段階的に導入するときには都合の良い性質です。ライブラリの CSS だけをレイヤーに入れてしまえば、いま書いてある自作のスタイルは1行も変えずに、常にライブラリより優先されるようになります。
下のデモでは、theme レイヤーの中に ID セレクター(詳細度 1,0,0)で赤を、レイヤー外にクラスセレクター(詳細度 0,1,0)で緑を指定しています。詳細度なら赤が圧勝ですが、実際の表示は緑です。.note のルールを @layer theme { ... } の中に移動すると、今度は詳細度の勝負になって赤に変わります。
<p id="msg" class="note">このテキストの色は?</p>
/* レイヤーに入れたルール。ID セレクターなので詳細度は 1,0,0 と非常に高い */
@layer theme {
#msg {
color: #dc2626; /* 赤 */
font-weight: bold;
}
}
/* レイヤーに入れていないルール。詳細度は 0,1,0(クラス1つ)と低いが……
レイヤー外は「すべてのレイヤーより強い」ので、こちらが勝って緑になる */
.note {
color: #16a34a; /* 緑 */
}
p {
font-size: 18px;
font-family: sans-serif;
}
優先順位の判定は何を先に見るのか
レイヤーが加わったことで、同じ要素に当たるスタイルの勝ち負けは次の順に決まります。上の段階で差が付いた時点で決着し、それ以降は見られません。
| 判定の順番 | 見るもの |
|---|---|
| 1 | !important が付いているか(付いているものが優先) |
| 2 | レイヤーの順番(!important なしなら、あとのレイヤー・レイヤー外が強い) |
| 3 | セレクターの詳細度(ID > クラス・属性・擬似クラス > 要素) |
| 4 | 記述順(同じ詳細度ならあとに書いたほうが勝つ) |
注意したいのは !important を付けたときの挙動です。!important が付いた宣言どうしでは、レイヤーの強弱が逆転します。通常は弱いはずの、先に宣言したレイヤーや reset のような最初のレイヤーが、!important の世界では最も強くなります。レイヤー外の !important は逆に最も弱い扱いです。仕様上そう決まっているのは、リセットや基盤の層が「どうしても譲れない値」を主張できるようにするためです。混乱の元なので、レイヤーを使うなら !important は極力使わない方針にしておくと安全です。
@import と組み合わせて外部 CSS をレイヤーに入れる
自分で書き換えられないライブラリの CSS をレイヤーに入れたいときは、@import に layer() を付けます。読み込んだファイルの中身がまるごと指定のレイヤーに入るため、ファイルを触らずに優先順位を下げられます。
/* 順番を先に決めておく */
@layer vendor, app;
/* ライブラリの CSS を vendor レイヤーに入れて読み込む */
@import url("library.css") layer(vendor);
/* 自作スタイルは app レイヤー。詳細度に関係なくライブラリより強い */
@layer app {
.button {
background: #2563eb;
}
}
@import は @layer の順序宣言や @charset を除き、スタイルシートの先頭に書く必要があります。他のルールより後ろに置くと無視されるので注意してください。なお layer を名前なしで書くと匿名レイヤーになり、あとから同じ層に追加できなくなります。基本は layer(名前) の形で名前を付けておくとよいでしょう。
<link> タグで読み込む CSS には、この方法は使えません。読み込んだ側のファイル全体をレイヤーに入れたい場合は、@import を使うか、そのファイル自身の先頭に @layer を書く必要があります。
レイヤーは入れ子にもできる
レイヤーの中にさらにレイヤーを作れます。入れ子のレイヤーはドットでつないで参照し、親レイヤーの中だけで順序を持ちます。規模が大きいプロジェクトで、components の中をさらに分類したいときに便利です。
@layer components {
/* components の中の順番を宣言する */
@layer base, card, button;
@layer button {
.button { padding: 8px 16px; }
}
}
/* 外から入れ子レイヤーを指すときはドットでつなぐ */
@layer components.card {
.card { border: 1px solid #ddd; }
}
revert-layer で下のレイヤーの値に戻す
レイヤーと一緒に使えるキーワードが revert-layer です。値としてこれを指定すると、そのプロパティは自分より前のレイヤーが決めた値まで巻き戻ります。前のレイヤーに指定がなければ、さらに前、最終的にはブラウザーの初期スタイルへとさかのぼります。
@layer base, theme;
@layer base {
a { color: #2563eb; }
}
@layer theme {
a { color: #dc2626; }
/* このリンクだけ base レイヤーの色(#2563eb)に戻す */
.plain-link { color: revert-layer; }
}
似たキーワードに revert がありますが、こちらは作成者のスタイル全体を打ち消してユーザーエージェント(ブラウザー標準)のスタイルまで戻します。「1つ前のレイヤーに戻したい」ときは revert-layer、「ブラウザー標準の見た目に戻したい」ときは revert と使い分けてください。
レイヤーを使っても上書きできないとき
上書きしたい側がレイヤー外に残っている
最も多いのがこのパターンです。自作スタイルをすべて app レイヤーに入れたつもりでも、どこか1か所だけレイヤーの外に書かれたルールが残っていると、それが全レイヤーより強く効いてしまいます。ブラウザーの開発者ツールでその要素のスタイルを見ると、適用されているルールの上に @layer の表示があるかどうかで判別できます。表示がなければレイヤー外のルールです。
レイヤー名の初出順が想定と違う
順序を先に宣言していない場合、レイヤーの並びは「その名前が最初に出てきた順」で確定します。複数の CSS ファイルを結合していたり、ビルドツールが読み込み順を並べ替えていたりすると、意図と違う順番になることがあります。ファイル分割をしているなら、順序を宣言する @layer a, b, c; の1行を必ずエントリーファイルの先頭に置いてください。
片方に !important が付いている
前述のとおり、!important が付いた宣言はレイヤーの強弱が逆転します。既存の CSS に残った !important のせいで、あとのレイヤーで上書きしたはずの値が効かない、ということが起こります。この場合はレイヤーの順番をいじるのではなく、原因となっている !important を外せないかを先に検討するほうが結果的に単純になります。
インライン style 属性で指定されている
HTML の style 属性による指定は、そもそもカスケードレイヤーの外側にある別枠の扱いで、通常のスタイルシートの宣言より強く働きます。JavaScript が要素に直接スタイルを書き込んでいるケースも同じです。レイヤーで解決しようとせず、その style 属性を出力している側を直してください。
まとめ
@layer は、スタイルを層に分けて優先順位を明示的に管理するための仕組みです。@layer 名前 { ... } でルールを層に入れ、@layer a, b, c; と書けば順序だけを先に宣言できます。あとに宣言したレイヤーほど強く、その勝負は詳細度より先に行われるため、ID セレクターで書かれたスタイルもクラス1つで上書きできます。一方で、どのレイヤーにも入れていないスタイルはすべてのレイヤーより強い点、!important が付くとレイヤーの強弱が逆転する点は必ず押さえておいてください。外部の CSS は @import url("...") layer(名前) で層に入れられ、revert-layer を使えば前のレイヤーの値まで巻き戻せます。まずはライブラリを1つレイヤーに入れてみるところから、少しずつ導入していくのがおすすめです。