CSSのネスト(入れ子)の書き方とつまずきやすい点

​​CSS​Playwright,Sass

CSS のネスト(入れ子)を使うと、.card の中の .title のスタイルを、.card { .title { … } } のように親のルールの中に書けます。Sass などのプリプロセッサーを通さなくても、ブラウザがそのまま読み取ります。ただし、見た目は Sass とほぼ同じでも、& の扱いや詳細度の計算が違うところがあり、そこで思ったとおりにスタイルが効かないことがあります。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。検証用のページを Chromium で開き、要素ごとに getComputedStyle() で実際に効いている値を調べました。あわせて、ブラウザが CSS をどう読み取ったかを、document.styleSheets のルールの一覧(CSSOM)で確かめています。Firefox と Safari は試していません。

基本の書き方

親のルールの中に、セレクターとスタイルをもう一組書きます。

css
.card {
  padding: 8px;
  .title { color: green; }
}

これは .card .title と書いたのと同じ意味になります。検証環境で試したところ、.card の中にある .title は緑になり、.card の外にある .title は色が変わりませんでした。

ブラウザが読み取ったルールを CSSOM で見ると、入れ子にした .title は、.card のルールの子として & .title という形で入っていました。

text
CSSStyleRule ".card"  { padding: 8px; }
  CSSStyleRule "& .title"  { color: rgb(0, 128, 0); }

& は、外側のルールのセレクター(ここでは .card)を表す記号です。MDN の & 入れ子セレクターのページにあるとおり、& を書かずに入れ子にすると、ブラウザは親のセレクターとの間に空白を入れて読みます。空白は「中にある要素」という意味なので、.title は .card の子孫の要素だけに当たります。

MDN の「CSS 入れ子の使用」によると、+ などの結合子も、そのまま入れ子の先頭に書けます。h2 { + p { … } } は h2 + p と同じ意味です。

:hover の前に & を付け忘れると、ボタン自身には効かない

いちばん間違えやすいのが、疑似クラスの前の & です。

css
.btn {
  :hover { background: #d85878; }
}

こう書くと、空白が入って .btn :hover という意味になります。つまり、「.btn の中にある要素のうち、マウスが重なっているもの」です。ボタン自身にマウスを重ねても、色は変わりません。ボタン自身の状態を指すには、& を空白なしで付けます。

css
.btn {
  &:hover { background: #d85878; }
}

検証環境で、2つの書き方のボタンにそれぞれマウスを重ねたところ、色が変わったのは &:hover と書いたほうだけでした。

2つの枠を縦に並べた画面。上の枠は「& を付けない」で、.btn の中に :hover を書いたコード。その下の「送信する」ボタンは、マウスを重ねた状態でも白いまま。下の枠は「& を付ける」で、.btn の中に &:hover を書いたコード。その下の「送信する」ボタンは、マウスを重ねた状態でピンクになっている
:hover の前に & がない場合とある場合(Chromium 153、どちらもボタンにマウスを重ねた状態)

CSSOM で見ても、& のないほうは & :hover と、間に空白の入ったセレクターとして読み取られていました。

同じことが、クラスを2つ重ねる書き方にも言えます。.tab { .active { … } } は .tab の中にある .active に、.tab { &.active { … } } は class="tab active" の要素自身に当たります。検証でも、前者のスタイルは中の要素にだけ、後者のスタイルはタブ自身にだけ効きました。

Sass のように & の後ろにクラス名の続きを書けない

Sass では、&__title と書くと、親のクラス名に文字をつなげて .block__title というクラスを作れます。BEM という命名の決まりでよく使う書き方です。CSS のネストでは、これができません。

css
.block {
  &__title { color: #d85878; }
}

検証環境では、このルールはまるごと無視されました。CSSOM を見ると、.block のルールの中に、入れ子のルールが1つも残っていません。

2つの枠を縦に並べた画面。上の枠は「Sass と同じつもりで書く」で、.block の中に &__title を書いたコード。その下の「記事の見出し」の文字は黒いまま。下の枠は「クラス名をそのまま書く」で、.block__title を入れ子にせずに書いたコード。その下の「記事の見出し」の文字はピンクになっている
&__title は効かず、クラス名をそのまま書くと効く(Chromium 153)

MDN の「CSS 入れ子の使用」には、CSS の入れ子では、結合子を使わない場合、入れ子のセレクターは要素型セレクターとして扱われると書かれています。&__title の __title は、__title という名前の要素を指すことになります。そして、要素型セレクターは複合セレクターの先頭に書く決まりなので、& の後ろにあると、そのセレクターとルールのブロック全体が無効になります。

CSS のネストで BEM のクラス名を使うときは、入れ子にせず .block__title と書くか、入れ子の中で & .block__title のようにクラス名を省略せずに書きます。検証環境でも、& .block__title の形で書いたルールは、親の要素の中の見出しに効きました。

要素名と & を組み合わせるときは、要素名を前に書く

同じ理由で、&section のように & の後ろに要素名を書くことはできません。要素名と & をつなげるときは、section& と書きます。

css
.post {
  section& { border-top: 3px solid blue; }
}

検証環境では、section& は <section class="post"> に効き、&section と書いたルールは無視されました。

一方、入れ子のルールを要素名から書き始めること(.post { p { … } })は、検証環境で問題なく効きました。MDN のブラウザ互換性データによると、Chrome は 112 から 119 まで、Safari は 16.5 から 17.1 まで、要素名で始まる入れ子のルールに対応していませんでした。MDN の「CSS 入れ子の使用」でも、& を付ける理由の1つに後方互換性が挙げられています。こうした古いブラウザで表示される可能性があるなら、& p のように & から書き始めておきます。

詳細度は :is() と同じ計算になる

CSS のネストでは、& の詳細度を :is() と同じように計算します。そのため、親のセレクターに詳細度の違うものがカンマで並んでいるときは、入れ子にせずにセレクターを1つずつ書いた場合と、詳細度が変わります。

MDN の「CSS 入れ子と詳細度」と同じ例で確かめました。

css
#a, b {
  & i { color: blue; }
}
.foo i { color: red; }
html
<b class="foo"><i>青になる</i></b>

<i> が入っているのは <b> なので、ID の #a は使われていません。それでも、& の詳細度は、並んだセレクターの中でいちばん高い #a のものになります。そのため & i が .foo i より強くなり、検証環境でも文字は青になりました。

比べるために、同じ形のルールを入れ子にせず #c i, u i と書き、.bar i と競わせたところ、こちらは赤になりました。u i は ID を含まないので、.bar i のほうが強いからです。セレクターを1つずつ書いていた CSS を入れ子に書き換えると、こうしてスタイルの勝ち負けが変わることがあります。入れ子の中のスタイルを、後から書いたクラスのセレクターで上書きしたいこともあります。そのときは、ID と要素名のように詳細度の違うセレクターを、別々のルールに分けて書きます。

& は疑似要素を表せない

MDN の & 入れ子セレクターのページにあるとおり、& は疑似要素を表せません。.note::before { .important & { … } } と書いても、::before には当たりません。検証でも、.important の中にある .note::before の色は変わりませんでした。

疑似要素は、& の後ろに付けて書きます。.note { .important &::before { … } } と書いたところ、.important の中にある .note::before の色が変わりました。

@media も入れ子にできる

@media や @supports、@container などは、ルールの中に入れ子にできます(MDN の「CSS 入れ子アットルール」)。中には、セレクターを書かずにプロパティを直接書けます。

css
.side {
  width: 300px;
  @media (width < 600px) { width: 100px; }
}

検証環境で表示領域の幅を変えたところ、幅 800px では 300px、幅 500px では 100px になりました。

入れ子のルールの後に書いたプロパティは、書いた順に効く

プロパティを、入れ子のルールより後ろに書くこともできます。

css
.order {
  color: red;
  @media screen { color: blue; }
  color: green;
}

検証環境では、文字は緑になりました。上から順に読んで、最後に書いた color: green が効いています。CSSOM で見ると、後ろの color: green は、.order 自身のスタイルではなく、CSSNestedDeclarations という別のルールとして、@media の後ろに入っていました。

text
CSSStyleRule ".order"  { color: rgb(255, 0, 0); }
  CSSMediaRule "screen"
    CSSNestedDeclarations  { color: rgb(0, 0, 255); }
  CSSNestedDeclarations  { color: rgb(0, 128, 0); }

CSS Nesting の仕様には、以前は入れ子のルールの後ろにあるプロパティも、ルールの先頭に移して扱っていたと書かれています。MDN の「CSS 入れ子の使用」にも、CSSNestedDeclarations に対応していないブラウザでは、入れ子のルールを間違った順序で解釈する場合があると書かれています。ブラウザ互換性データでは、対応したのは Chrome 130、Firefox 132、Safari 18.2 です。どのブラウザでも同じ結果にしたいなら、プロパティは入れ子のルールより前にまとめて書きます。

無効なセレクターがあっても、ほかのルールは残る

入れ子のルールのセレクターが無効なとき、無視されるのはそのルールだけです。

css
.parent {
  & %invalid { color: red; }
  & .valid { color: green; }
}

検証環境では、% を含むルールは CSSOM から消えていましたが、その後ろの & .valid は残り、.valid の文字は緑になりました。&__title や &section のルールが消えたのも、同じ仕組みです。入れ子のスタイルが効かないときは、document.styleSheets の cssRules をたどって、そのルールが残っているかを確かめると、書き方の間違いに気づけます。

​Posted by 86