🔖

なぜ私の1frはフレックスコンテナをはみ出すのか

2024/06/27に公開

CSSのGrid Layoutを書いていて度々悩まされるこんな問題。

1fr で 3 列の等幅な列を作ることがこのレイアウトの意図でしたが、フレックスアイテムのコンテンツが折り返されず、フレックスコンテナを飛び出して横方向のスクロールが発生してしまっています。

レビューでは、minmax(0, 1fr)にしといてねとか、フレックスアイテムの方にmin-width: 0をつけてねとか言って簡単に済ますんですが、こういうのって経験的に知っているだけで、なんでその問題が起きるのか、そしてなんでこの方法で直るのか、といった細かい理由はよく知らなかったのです。

まぁ正直理由なんか知らなくても直るならいいんじゃないか、とも思いますが、会社のフロントエンド雑談会でちょうど話す機会があったので、最新の W3C Candidate Recommendation Draftを見て、以下のようなCSSを書いた時に何が起きているのか、そしてなぜminmax(0, 1fr)min-width: 0で期待どおり等幅に戻るのか、その仕様を調べてみました。

.grid {
  grid-template-columns: repeat(3, 1fr);
}

CSS grid の Track Sizes の仕様

Flex factor (fr unit) と暗黙の自動最小サイズ auto

まずはじめに、Track Sizesの'<flex>'の項を見ると、そこには以下のように書かれています。

A non-negative dimension with the unit fr specifying the track’s flex factor. Each <flex>-sized track takes a share of the remaining space in proportion to its flex factor. For example, given a track listing of 1fr 2fr, the tracks will take up ⅓ and ⅔ of the leftover space, respectively. See § 7.2.4 Flexible Lengths: the fr unit for more details.(略)

When appearing outside a minmax() notation, implies an automatic minimum (i.e. ''minmax(auto, <flex>)'').

これによると、minmaxで囲わないで使用する 1frは、minmax(auto, 1fr) と書いた場合と同義になるようです。つまり最初の CSS は以下のように書き換えることができます。

.grid {
  grid-template-columns: repeat(3, minmax(auto, 1fr)); /* repeat(3, 1fr)と同義 */
}

minmax関数と下限(flooring)の性質

それでは、記述にある automatic minimum、つまり minmax 関数の min 値autoとはどんなサイズに決まるのでしょう。それを知るために、まずはTrack Sizesの'minmax(min, max)'の項を読んでみます。

Defines a size range greater than or equal to min and less than or equal to max. If the max is less than the min, then the max will be floored by the min (essentially yielding minmax(min, min)).

minmax 関数は min, max 2 つの引数を受け取り、列(または行)の最小最大サイズを規定します。もし max が min よりも小さい場合、max の大きさは min の大きさまで下限されます。

ではminmax(auto, 1fr) と書いた場合、auto1frはどちらが大きいのかを考えてみましょう。

先に分かりやすい方として、1frの方を考えます。fr(引用元で言う flex factor ) はコンテナの余ったスペースを、fr の大きさの割合によって分け合います。今回の例だとrepeat(3, 1fr)と指定しており、フレックスコンテナの幅(ここではスクリーン幅)を3つの1frが均等に分け合います。つまり1frのサイズはスクリーン幅の1/3です。

最小値としての auto はどんなサイズになるのか

それでは、minの値であるautoのサイズはどう決まるのでしょうか。その仕様については'minmax(min, max)'の次にある'auto'の項に書かれていました。

As a minimum: represents the largest minimum size (specified by min-width/min-height) of the grid items occupying the grid track. (This initially is often, but not always, equal to a min-content minimum—see § 6.6 Automatic Minimum Size of Grid Items.)

largest minimum size という表現が少々わかりにくいですが、ある列(または行)に含まれる各グリッドアイテムのmin-widthの最大値が、その列(または行)の最小サイズとして決まるようです。さらに、多くの場合minmax(auto, 1fr)minmax(min-content, 1fr)と書いた時と等しいと書かれています。

この説明のうち、"largest minimum size (specified by min-width/min-height)" という説明を読んでちょっと引っかかるところがありますが、それはこの後掘り下げるとして、一旦ドキュメントに従って上のCSSを更に書き換えてみます。

.grid {
  grid-template-columns: repeat(3, minmax(min-content, 1fr)); /* repeat(3, 1fr) と多くの場合で等しい */ 
}

こう書き換えることで何となく理解ができてきました。minmax の最小サイズは各グリッドアイテムのmin-contentのサイズとなり、これは各グリッドアイテムのコンテンツ量によって変動します。一方、最大サイズは1frで、これは先述したとおりスクリーン幅の 1/3 です。

今回の例ですと真ん中の列について、最小サイズ('http://.....'というコンテンツのmin-contentのサイズ)が最大サイズ(1frのサイズ)よりも大きくなったため、最大サイズは最小サイズに下限され、結果その列幅が'http://.....'というコンテンツのmin-contentのサイズに決定されたようです。

最小サイズ min-width のデフォルト値って 0 じゃなかったっけ

先ほど引用した'auto'の項に関する定義で、"largest minimum size (specified by min-width/min-height)" という記述がありました。さらに続けて、"This initially is often, but not always, equal to a min-content minimum" とも書かれています。

意訳すると、minmax の min 値をautoにした場合の最小サイズは min-width のサイズになり、そして多くの場合、それは min 値にmin-contentを指定した時と等しいということのようです。

しかし今回の例では min-width は指定しておらず、min-width を指定しない場合のデフォルト値は 0 だったんじゃないかと疑問に思います。つまり、minmax の min 値をautoにした場合の最小サイズは 0 であり、min-contentのサイズではないんじゃないかということです。

その疑問に対する答えが 6.6. Automatic Minimum Size of Grid Items に記載されているのでもう少し追いかけてみます。

Gridの世界では、自動最小サイズは 0 ではない場合がある

まず冒頭で、以下のように述べられています。

To provide a more reasonable default minimum size for grid items, the used value of its automatic minimum size in a given axis is the content-based minimum size if all of the following are true:

  • it is not a scroll container
  • it spans at least one track in that axis whose min track sizing function is auto
  • if it spans more than one track in that axis, none of those tracks are flexible

Otherwise, the automatic minimum size is zero, as usual.

細かい説明は省きますが、automatic minimum size は通常 0 なのですが、上記 3 条件を満たす場合には content-based minimum size に決定されます。

automatic minimum size とは、minmax の min 値をautoにした場合の最小サイズや、min-width を指定しない場合の最小サイズのことと考えていいでしょう。つまり、グリッドの世界では、(一定の条件下で)こうした最小サイズが 0 ではなく content-based minimum size に規定されるようです。

ちなみに、content-based minimum size の定義については続けてこう書かれています。

The content-based minimum size for a grid item in a given dimension is its specified size suggestion if it exists, otherwise its transferred size suggestion if that exists, else its content size suggestion, see below. (略)

  • specified size suggestion
    If the item’s preferred size in the relevant axis is definite, then the specified size suggestion is that size. It is otherwise undefined.
  • transferred size suggestion
    If the item has a preferred aspect ratio and its preferred size in the opposite axis is definite, then the transferred size suggestion is that size (clamped by the opposite-axis minimum and maximum sizes if they are definite), converted through the aspect ratio. It is otherwise undefined.
  • content size suggestion
    The content size suggestion is the min-content size in the relevant axis, clamped, if it has a preferred aspect ratio, by any definite opposite-axis minimum and maximum sizes converted through the aspect ratio.

実際にはこの引用に加えていくつかの制約などが書かれていますが、今回の問いに答えるには無視できそうなので割愛します。(正しく日本語で説明できる自信もない。)

specified size suggestion の定義から、もしグリッドアイテムに min-width が設定されていれば、その値が automatic minimum size となります。transferred size suggestion の定義の説明は割愛しますが、aspect-ratio が設定されている時にはこちらのルールによって automatic minimum size が決まるんじゃないかと思います。

そして上記 2 条件に当てはまらない場合には、content size suggestion の定義から、グリッドアイテムの min-content のサイズが automatic minimum size として決まります。

まとめ

単純にトラックサイズに1frと書いた場合、暗黙的に最小サイズのautoが登場し、その最小サイズは content-based minimum size の定義に従って、多くの場合グリッドアイテムの min-content のサイズをもとに決まることを学びました。

.grid {
  grid-template-columns: repeat(3, 1fr); 
  /* grid-template-columns: repeat(3, minmax(auto, 1fr)); 等しい */ 
  /* grid-template-columns: repeat(3, minmax(min-content, 1fr)); 多くの場合で等しい */ 
}

そして、これまで経験的に知っていた、minmax(0, 1fr)に変えたりフレックスアイテムにmin-width: 0を設定する対応策についても、なぜそれが有効か説明できるようになりました。

前者は、明示的にminmax(0, 1fr)と指定することで、暗黙的なminmax(auto, 1fr)を抑制する方法です。最小サイズの0と最大サイズの1frでは必ず最小サイズ < 最大サイズの関係が成り立ちますから、全ての列を1frのサイズで等幅にすることが出来ます。

後者は、content-based minimum size を specified size suggestion によって 0 にする方法です。先述したとおり、specified size suggestion は content size suggestion よりも優先しますから、min-width: 0を設定することで最小サイズを 0 とし、min-content のサイズになることを避けることが出来ます。

最後に

こんなこと知っていなくても業務に支障はありません。

出典: https://www.w3.org/TR/2020/CRD-css-grid-2-20201218/

株式会社StoreHero

Discussion