カードは自分がどこに置かれたか知らない
一言でいうと
flexは1つの軸を分け、gridは2つの軸を同時に決めます。そして、レスポンシブの本当の問いは「画面が何pxか」ではなく、このボックスが何pxかです。その問いに答えるのが、コンテナークエリです。
なぜ必要なのか
カードのコンポーネントを1つ作ります。狭いときは縦に、広いときは横に並ぶようにしたくて、メディアクエリを使います。
.card { display: block; }
@media (min-width: 700px) { .card { display: flex; } }
このカードを本文に置くと、うまく動作します。ところが、同じカードを狭いサイドバーに置くと、壊れます。画面は1400pxなのでメディアクエリは真ですが、カードが収まっているボックスは280pxだからです。
コンポーネントは、自分がどこに置かれたかを知りません。メディアクエリは画面の幅しか問いませんが、コンポーネントが知る必要があるのは、自分のボックスの幅です。このずれが、デザインシステムで最もよく起きる構造的な問題でした。
どう動くのか: flexとgrid
どちらを使うかは、好みではなく次元で分かれます。MDNの比較ドキュメントが、この関係を整理しています。
| 状況 | 適したもの |
|---|---|
| 1行(または1列)の中で分ける | flex |
| 内容の大きさに応じて自動で配分する | flex |
| 行と列を両方決める | grid |
| 画面の骨格: ヘッダー・サイド・本文・フッター | grid |
| 個数が変わっても、位置を守る必要がある | grid |
flexは内容から出発します。アイテムが自分の大きさを決め、余った分や足りない分を分けます。gridは枠から出発します。先に列を決め、アイテムをその列に入れます。
そのため、ツールバーやボタンの行のように「中に何が入るかわからない1行」にはflexが合い、ページの骨格のように「列が先に決まっているもの」にはgridが合います。
flexアイテムが縮まない問題
flexで最もよく出会う落とし穴があります。長い文章や幅の広い表が入ったアイテムが、縮まずにコンテナーを押し広げることです。
原因は、flexアイテムのmin-widthの初期値がautoだということです。この値は「内容の最小サイズより小さくならない」という意味なので、折り返せない長い文字列があれば、その分は必ず占めます。MDNのflex比率のドキュメントが、この動作を説明しています。
.item { min-width: 0; } /* 또는 overflow: hidden */
この1行が、「なぜ横スクロールが出るのか」の答えであることが、非常に多くあります。
flex: 1は、flex-grow: 1; flex-shrink: 1; flex-basis: 0%の省略形です。flex-basis: 0%なので、内容の大きさを無視して、余った領域を等しく分けます。flex: autoはflex-basis: autoなので、内容の大きさから出発します。この2つを混ぜて使うと、予想と違う結果になります。
gridで、列の数を数えずに配置する
gridには、メディアクエリなしで反応する定型表現があります。
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
「最小240pxの列を、入るだけ作り、余った場所は等しく分けなさい」という意味です。画面の幅を数えなくても、列の数が自動で変わります。CSS Grid 2が、auto-fitとauto-fillの違いを定義しています。auto-fillは空の列を残し、auto-fitは空の列を折りたたんで、残ったアイテムが広がります。
コンテナークエリ
それでも残る問題があります。上の定型表現は列の数は変えますが、カードの中の配置は変えられません。カードが240pxのときと600pxのときで内側の構造が違う必要があるなら、カードが自分の幅を問う必要があります。
.card-wrap { container-type: inline-size; }
@container (min-width: 420px) {
.card { display: grid; grid-template-columns: 120px 1fr; }
}
container-type: inline-sizeは、「この要素のインライン方向のサイズを、クエリの対象にします」という宣言です。すると、その子孫が@containerで、祖先の幅を問えるようになります。CSS Containment 3が定義し、MDNのコンテナークエリのドキュメントが使い方を整理しています。
2つのことを覚えておきます。
- 自分自身には問えません: コンテナーとして宣言した要素ではなく、その内側の要素にルールを書きます。そのため、ラッパーが1層必要です
container-type: inline-sizeは、サイズの封じ込め(size containment)を有効にします。その方向で、子が親のサイズに影響しなくなるため、高さを内容に合わせていた場所で、予想外の結果が出ることがあります
現場での姿
実際に経験したことが3つあります。
1つ目は、同じコンポーネントが2か所で異なる壊れ方をすることです。本文では問題なく、サイドバーでだけ壊れます。これは、ほとんどいつも、メディアクエリを使うべき場所に、コンテナークエリが必要だったケースです。
2つ目は、横スクロールが出たのに犯人が見つからないことです。flexアイテムのmin-width: autoが原因であることが最も多く、次がwidth: 100vw(スクロールバーの幅が含まれます)です。
3つ目は、メディアクエリのブレークポイントが10個を超えることです。これはたいてい、コンポーネントが画面の幅を問う理由がないのに、問い合わせているというサインです。repeat(auto-fit, minmax(...))とコンテナークエリに移すと、ブレークポイントの大部分がなくなります。
次のクイズで確認すること
flexとgridの使い分け、flex: 1とflex: autoの違い、min-width: autoが生むはみ出し、auto-fitとauto-fillの違い、そして、コンテナークエリがメディアクエリと何が違うのかを確認します。