🔢

「子孫要素の中でもっともサイズの大きい要素」のサイズを CSS のみで利用する

2024/06/05に公開

はじめに

CSS のみでもっともサイズの大きい子孫要素のサイズを元に calc() で計算して特定の倍数などにしたくなる時ってありませんか?ほとんどありませんね。
今回は自分が個人開発中にたまたま見つけたテクニックを紹介します。
また、このニッチなケースを抜きにしてもこれは「なぜ width: 120%; のように 100% 以上の値を指定しても親要素のサイズが変動しないのか?」という疑問の答えでもあります。
ちなみに、flex とか grid では倍数云々が実現できなかったので使いません。もし他にもっとシンプルな実現方法があればぜひ教えて下さい。今回紹介するやり方は正直自分でも非直感的だと思うので・・・。

例えば、こういうコードがあったとします。

<div class="popup">
  <ul class="popup-menu">
    <li class="popup-menu-item">Item #1</li>
    <li class="popup-menu-item">Item #2</li>
    <!-- .popup-menu-item がいくつか続く -->
  </ul>
</div>

ここで、.popup-menu-item 要素のもっとも width が大きい要素の横幅をベースに計算した値を .popup-menu の width を設定したいとしましょう。
.popup-menu-item では Item #1 のように中の数字の増減するだとか、文字数がバラバラのテキストが入ったりするだとかでサイズが変動する可能性があるとします。
今回の例では「.popup-menu 要素の width を子要素をはみ出させずに(特定の数字)の倍数で設定したい」とします。たぶんなかなかニッチなケースですね。
これを CSS のみで、CSS の仕様通りの動作で実現することができます。

ということで、なぜこれが実現できるのかを CSS の仕様から探っていきますがその前に TLDR を。

TLDR

CSS の width / height が auto であった時、子孫要素の単位が <length> の値で指定された margin / padding / width / height を元に計算されます。
パーセントで指定された値は無視されます。
ただパーセントで指定された要素のサイズが親要素より大きくなるとオーバーフローするので対策が必要になるかもしれませんが、その場合でも親要素の max-width / max-height に収まるように気をつければオーバーフローすることはありません。

今回のケースは下記のような CSS で実現可能です。

/*
.popup の width は auto となる必要がありますが、
min-width / max-width などは使用しても問題ありません。
ここでは省略していますが前述の通り .popup-menu の横幅 / 縦幅がオーバーフローする可能性が高いのでうまいこと対策してください。
今回は 50 の倍数に設定したいので、max-width で適当に 50 * 3 の 150px を設定します。
*/
.popup {
  /* width: auto; */
  max-width: 150px;
}

.popup-menu {
  /* 上述の通り 50 の倍数に設定します */
  /* round(): https://developer.mozilla.org/en-US/docs/Web/CSS/round */
  width: round(up, 100%, 50px);
  /* フォールバック */
  width: calc((100% / 50px) * 50px);
}

/* .popup-menu-item の width は 150px を上限として自動で 50 の倍数となるよう引き伸ばされます */

CSS の仕様

ではさっそく CSS の仕様(の日本語訳)を見ていきましょう。
下記は CSS Box Sizing Module Level 3 の 「5. 内在的サイズの決定」から、サイズを計算する式の仕様が書かれている「5.1. 内在的サイズ」の一部からの引用です。

各軸に対し — 以下, “サイズ” は、 その軸におけるサイズを表す — ボックスの[ 最小内容サイズ/最大内容サイズ ]は、 浮動体が[ サイズ 0 の / サイズ無限な ]包含ブロック内で, その選好サイズに auto が与えられていた (かつ, 最小サイズ, 最大サイズどちらも与えられていない) とするとき占めるサイズになる (言い換えれば、 “収まるよう縮短した” ときの[ 最小サイズ/最大サイズ ])。
[ 最小内容サイズ/最大内容サイズ ]は、 内在的サイズと総称される。

つまり「内在的サイズ = 要素の中身をベースに決まるサイズ」ってことです。たぶん。
ここでは

その選好サイズに auto が与えられていた (かつ, 最小サイズ, 最大サイズどちらも与えられていない) とするとき占めるサイズになる

がポイントです。

中略してまた引用します。

ボックスは選好縦横比を伴わない場合:
[ 最小内容サイズ, 最大内容サイズ ]どちらも:
当の次元におけるボックスの最小サイズ ( min-width / min-height ) の算出値は <length> になるならば、 そのサイズを利用する。
注記: この[ 作者が制御可能なふるまい ]は、 最小サイズプロパティ用の新たな値 auto により可能になった。 これは,より良いふるまいを与えるものと予見されているが、 Web 互換かどうかは,まだ明瞭でない — 問題があるなら,CSS WG にフィードバックを送信されたし。
他の場合、 当の次元が[ 横幅ならば 300px / 縦幅ならば 150px ]を利用する。

[ ブロックレベル/行内レベル ]の置換される要素のうち[ height, width ]いずれかが auto としてふるまうものは[ 実質的には, 要素の最大内容サイズを利用するよう定義される ]ので ( [CSS2] § 10.3.2 )、 この仕様は,未定義であった[ 置換される要素が[ height, width どちらも auto としてふるまう ]事例 ]に上の規則を適用する。

とあります。ここでは

[ 最小内容サイズ, 最大内容サイズ ]どちらも:
当の次元におけるボックスの最小サイズ ( min-width / min-height ) の算出値は <length> になるならば、 そのサイズを利用する。

[ ブロックレベル/行内レベル ]の置換される要素のうち[ height, width ]いずれかが auto としてふるまうものは[ 実質的には, 要素の最大内容サイズを利用するよう定義される ]ので ( [CSS2] § 10.3.2 )...

がポイントです。
つまり「width / height が auto の時で、要素の内容(子孫要素を含む)のサイズの単位が <length> である場合、それを使用する」ってことです。たぶん。
選好縦横比とはなんぞや?となりますが、同ページに「ほとんどのボックスには、選好縦横比は無い。」と書かれていますので一旦無視します。

"<length>", "単位が <length> の値" とは?

詳しい説明は MDN に任せますが、簡単に言えば「pxremvw など、長さを指定する時に使われるパーセント以外の値、またはその値の単位」です。

参考:
MDN の「CSS 値と単位 #長さ」についてのページ
MDN の「<length>」についてのページ

つまり?

つまり、「親要素の width / height が auto で、サイズを利用したい子孫要素のサイズがパーセントで指定されている場合、それは無視されて他のパーセントで指定されていない子孫要素のサイズを元に親要素のサイズが設定されるから親要素の直下の子要素で『width: 100%;』を使えばもっとも(略)子孫要素のサイズを利用できるし 100% 以上の値を指定したり calc() での計算結果が 100% 以上になっても親要素のサイズはその影響を受けないよ」ってことです。

おわり。

余談: なんで倍数にしたくなったの?

最近、自分はコーディングの時や映画・動画を見る時以外はほとんどスマホで見ているので、自分用の Web アプリをスマホ専用のレイアウトで作ろうと開発を始めました。
スマホ用のデザインシステムでは、Android ユーザーなのもあり Material Design が好きなのそれに準拠したものを作ろうとしていたのですが、そこで「Menu コンポーネントの横幅は中の MenuItem の横幅が収まるように 56 の倍数で設定する(現在の M3 より古いバージョンである M1 のドキュメント)」との記述を見つけました。
(M3 のドキュメントには MenuItem のサイズについての情報が載っていなかったので古い M1 のドキュメントを参考にしていますが、Material Design と同じく Google 製である Flutter でもこれに準拠するよう実装されているので今の M3 でも同じだと思います)
(というか、Material Design のドキュメントは主に公式でこのデザインシステムがサポートされている Android で使用することを想定されているのもあり、他のプラットフォームで使用する際には細かいところは過去のバージョンのドキュメントや実装コードを見ないとわからない点がよく出てきます)

もちろん公式の Material Web コンポーネントや MUI の存在は知っていたのですが、このどちらもその「自動で 56 の倍数に設定する」仕様に準拠していないっぽいのでなんとか自分で実装方法を見つけました。

grid で出来ないかとか試行錯誤したりしてこれを実現するためだけに数時間取られました。
JS の getBoundingClientRect() 使えばいい?それはそう。
でも、CSS だけで実現させるというロマンがそこにはあったのです。

Discussion