CSSで、中の項目の数に合わせてレイアウトを変える方法
リストやカードの並びを、中の項目の数で変えたいときは、親要素に :has() と :nth-child() を組み合わせたセレクターを書きます。「2番目の子がある」なら2個以上、「最後の子が3番目」ならちょうど3個、というように数を判定でき、JavaScript で数を数えてクラスを付ける必要はありません。項目をあとから足したり消したりしても、レイアウトはその場で切り替わります。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。幅 600px のリスト(項目の間は 8px)を Playwright で開き、各項目の位置と幅、親の grid-template-columns の計算値を測りました。Firefox と Safari での表示は試していません。各ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。
:has() で、親から子の数を判定する
:has() は、かっこの中のセレクターに一致する要素を持っている親を選ぶ擬似クラスです。かっこの中に :nth-child() を書くと、子の数で親を選べます。
| 書き方 | 一致する条件 |
|---|---|
.cards:has(> :nth-child(2)) |
子が2個以上 |
.cards:has(> :last-child:nth-child(3)) |
子がちょうど3個 |
.cards:has(> :nth-child(5)) |
子が5個以上 |
2個以上の判定は、「2番目の子がいる」ことで決まります。ちょうど3個は、「最後の子が先頭から3番目でもある」ことで決まります。
この書き方で、項目が1個なら1列、2個なら2列、3個なら3列、4個なら2列を2段、5個以上なら3列にするカードの並びを作りました。
.cards {
display: grid;
gap: 8px;
grid-template-columns: repeat(var(--cols, 1), 1fr);
}
/* 2個以上なら2列 */
.cards:has(> :nth-child(2)) {
--cols: 2;
}
/* ちょうど3個なら3列 */
.cards:has(> :last-child:nth-child(3)) {
--cols: 3;
}
/* 5個以上なら3列 */
.cards:has(> :nth-child(5)) {
--cols: 3;
}
列の数はカスタムプロパティ --cols に入れ、条件ごとにその値だけを変えています。4個のときは「2個以上」の指定だけが当てはまるので、2列のまま2段になります。

検証環境で項目の数を1個から7個まで変えて測ると、各項目の幅は次のようになりました。
| 項目の数 | 並び方 | 1つの項目の幅 |
|---|---|---|
| 1個 | 1列 | 600px |
| 2個 | 2列 | 296px |
| 3個 | 3列 | 194.7px |
| 4個 | 2列×2段 | 296px |
| 5〜7個 | 3列(7個は3段目に1個) | 194.7px |
項目を足したり消したりしても、その場で切り替わる
項目を JavaScript で1個ずつ足していくと、そのたびに列の数が変わりました。
const li = document.createElement('li');
li.textContent = '項目5';
document.querySelector('.cards').append(li);
空のリストから1個ずつ足すと、1列、2列、3列、2列×2段、3列の順に変わり、上の表と同じ並びになりました。5個から最後の1個を消すと、2列×2段に戻りました。項目の数を数えてクラスを付け替えるような JavaScript は、どこにも書いていません。
hidden の項目も数に入る
:nth-child() は、hidden 属性で隠した子も数えます。4個のうち1個に hidden 属性を付けて、見えている項目を3個にしたところ、上の CSS では3列にならず、2列で2段目に1個だけ残る並びになりました。
見えている項目だけを数えたいときは、:nth-child() の of を使う書き方で、数える対象を絞ります。:nth-child(4 of :not([hidden])) は、「hidden が付いていない子のうちの4番目」に一致します。ちょうど何個かを判定するときは、:last-child の代わりに、後ろから数える :nth-last-child() を使って :nth-last-child(1 of :not([hidden])) と書きます。こうすると、後ろから数えるときも hidden のない子だけが数に入ります。
.cards:has(> :nth-child(2 of :not([hidden]))) {
--cols: 2;
}
.cards:has(> :nth-child(3 of :not([hidden])):nth-last-child(1 of :not([hidden]))) {
--cols: 3;
}
.cards:has(> :nth-child(5 of :not([hidden]))) {
--cols: 3;
}
この書き方にすると、4個のうち1個が hidden のリストは、隠した項目が末尾でも先頭でも3列になりました。hidden のない1〜5個のリストでは、最初の CSS と同じ並びのままでした。
かっこの中の > を省くと、項目の中の要素まで数える
:has(> :nth-child(2)) の > は、親の直接の子だけを調べるための記号です。これを省いて .cards:has(:nth-child(2)) と書くと、子の中にある要素まで探しに行きます。
項目が1個だけのリストで、その項目の中に小さなリスト(li が2個)を入れて試しました。> を省いた書き方では、項目の中の小さなリストの2個目の li が「2番目の子」に当たり、項目1個なのに2列になって、項目の幅が 296px に狭まりました。> を付けた書き方では、1列の 600px のままでした。項目の中にリストやボタンなどの要素が並ぶ場合は、> を必ず付けておきます。
子の側から数える書き方
:has() が使えないブラウザーにも合わせたいときは、子の側から数える方法があります。:first-child:nth-last-child(n+4) は、「先頭の子で、かつ後ろから数えて4番目以降」、つまり兄弟が4個以上あるときの先頭の子に一致します。そこから ~ で後ろの兄弟もまとめて選びます。
.cards {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.cards > li {
flex: 1 1 0;
}
/* 4個以上なら、1つの項目を半分の幅にする */
.cards > li:first-child:nth-last-child(n+4),
.cards > li:first-child:nth-last-child(n+4) ~ li {
flex: 0 0 calc(50% - 4px);
}
検証環境では、3個までは横1列に均等に並び(3個なら 194.7px ずつ)、4個と5個では 296px ずつの2列で折り返しました。
この書き方で変えられるのは、子のスタイルだけです。親の display や grid-template-columns は変えられないので、上の例でも親は flex のまま、子の幅で列のように見せています。:has() なら、親のほうの指定を直接切り替えられます。
sibling-count() で、数をそのまま計算に使う
新しいブラウザーだけでよければ、sibling-count() 関数も使えます。自分を含めた兄弟の要素の数を整数で返すので、calc() の中で割り算に使えます。
.cards {
display: flex;
gap: 8px;
}
.cards > li {
width: calc((100% - (sibling-count() - 1) * 8px) / sibling-count());
}
項目の間の 8px を引いた残りを、項目の数で割っています。検証環境では、1個なら 600px、2個なら 296px、4個なら 144px、5個なら 113.6px と、1〜5個のどれでも横1列に均等に並びました。「何個以上なら何列」のように段階を分けるのではなく、数に応じて1つの項目の幅を細かく変えたいときに向いています。
sibling-count() も、hidden の項目を数に入れました。4個のうち1個を hidden にすると、見えている3個は 144px(4個のときの幅)のままでした。3個の幅と、間の 8px 2つを足しても 448px なので、600px の幅のうち 152px が余ります。sibling-count() には of のような絞り込みの書き方はありません。
項目が0個のときは :empty より :not(:has(> *))
項目がないときに「まだ項目がありません」と出したい場合は、子の要素がないことで判定します。
.cards:not(:has(> *))::before {
content: "まだ項目がありません";
}
:empty でも空の要素を選べますが、空白や改行の文字も子として数えます。検証環境で、<ul></ul> と、中にコメントだけがある ul は :empty に一致しましたが、中に改行だけがある ul は一致しませんでした。HTML を整形して <ul> と </ul> を別の行に書くと、それだけで :empty が効かなくなります。:not(:has(> *)) は、3つとも一致しました。
ブラウザーの対応
互換性データでは、対応しているバージョンは次のとおりです。
| 機能 | Chrome・Edge | Firefox | Safari(iOS を含む) |
|---|---|---|---|
:has() |
105 | 121 | 15.4 |
:nth-child() の of |
111 | 113 | 9 |
:nth-last-child() |
Chrome 4・Edge 12 | 3.5 | 3.1(iOS は 2) |
sibling-count() |
138 | 154 | 26.2 |
sibling-count() は、:has() よりずっと新しいバージョンから対応しています。古いバージョンのブラウザーで見る人もいる場合は、:has() で段階ごとに列数を切り替える書き方を選びます。





