300pxが300pxでない理由
一言でいうと
width: 300pxは、標準の設定ではコンテンツ領域の幅であって、ボックスの幅ではありません。そして、上下のマージンは足されるのではなく相殺されます。この2つが、レイアウトが崩れる事故の大部分です。
なぜ必要なのか
「2つの要素を50%ずつ置いたのに、改行されてしまう」「パディングを入れたら、カードがグリッドの外にはみ出す」「上の要素にmargin-bottom: 20px、下の要素にmargin-top: 30pxを指定したのに、間隔が50pxではなく30pxになる」
3つとも、CSSを知らないからではなく、モデルを知らないまま値を入れたために起きることです。そして、3つとも、目で見ると「なぜこうなるのか」としか思えません。計算のルールを知っていれば、その場で答えが出ます。
どう動くのか: ボックスの4つの層
┌─────────── margin ───────────┐
│ ┌───────── border ─────────┐ │
│ │ ┌─────── padding ──────┐ │ │
│ │ │ content │ │ │
│ │ └──────────────────────┘ │ │
│ └──────────────────────────┘ │
└──────────────────────────────┘
CSS 2仕様のボックスモデルが定義した4つの層です。標準のbox-sizing: content-boxでは、widthは、いちばん内側のcontentだけを指します。そのため、
.card { width: 300px; padding: 20px; border: 1px solid; }
このカードが占める横幅は、300 + 20 + 20 + 1 + 1 = 342pxです。親が300pxなら、はみ出します。
box-sizing: border-boxを指定すると、widthがborderまで含んだ値になります。上のカードはちょうど300pxになり、コンテンツ領域が258pxに縮みます。MDNのbox-sizingのドキュメントが、2つのモードを比較しています。
そのため、ほとんどすべてのプロジェクトが、いちばん上にこの3行を置きます。
*, *::before, *::after { box-sizing: border-box; }
marginは、どちらのモードでもwidthに含まれません。これだけは例外がありません。
マージンの相殺: 足されず、合わさります
上下に接したマージンは、足されずに、2つのうち大きいほうの値1つになります。MDNのマージン相殺のドキュメントが、3つのケースを整理しています。
- 隣り合う兄弟: 上の要素の下マージンと、下の要素の上マージンが合わさります
- 親と最初/最後の子: 間にボーダーもパディングもなければ、子のマージンが親の外へ漏れ出します
- 空の要素: 自分の上下のマージンが、自分同士で合わさります
2つ目が、最も人を戸惑わせます。親ボックスの内側に余白を与えようとして、子にmargin-topを指定したのに、親がまるごと下へ押されます。背景色があると、その色がついてこないので、さらにおかしく見えます。
相殺を止める方法は、間に何かを置くことです。親にpadding-topを1行、border-topを1行、またはdisplay: flow-rootです。新しいブロックフォーマッティングコンテキスト(BFC)を作ると、マージンは中に閉じ込められます。
そして、横方向のマージンは絶対に相殺されません。左右は、常に足されます。
flexとgridの中では相殺がありません
マージンの相殺は、通常のフロー(blockの書式設定コンテキスト)のルールです。flexアイテムとgridアイテムの間では起きません。
そのため、最近のレイアウトでは、相殺に出会う機会が減りました。その代わりに、gapがその場所を引き受けます。gapはアイテムの間にだけ入り、外側には入らないため、「最後の要素だけマージンを除く」というルールが、まるごと不要になります。
.list { display: flex; flex-direction: column; gap: 16px; }
この1行が、.list > * + * { margin-top: 16px }の代わりになります。
値はどこを基準に測るのか
パーセンテージは、包含ブロックの幅を基準にします。ここに落とし穴が1つあります。padding-topとmargin-topのパーセンテージも、幅を基準に測ります。高さではありません。縦横比を維持するボックスを作るときに、padding-top: 56.25%を使っていた古い手法は、ここから生まれました(今はaspect-ratioがあります)。
そして、min-widthとmax-widthは、widthよりもあとに適用されます。width: 100%とmax-width: 640pxを一緒に使うと、640pxを超えない可変ボックスになります。CSS Valuesが、単位と計算の順序を定義しています。
現場での姿
このサービスで実際に起きたことを、2つ書き留めておきます。
1つ目は、本文だけが画面の外にはみ出したことです。bodyをflex columnにして、本文にmargin: 0 autoを指定したところ、flexの子に自動マージンが付くと、引き伸ばし(stretch)が無効になります。幅の広いコードブロックが1つ入ってくると、本文だけがその幅に広がり、上部のバーとずれました。width: 100%を明示して直しました。
2つ目は、カードのグリッドが1行に2つしか入らなかったことです。width: 33.33%にpadding: 16pxを指定したのに、box-sizingが初期値のままでした。3つの合計が100%を超えて、改行されました。border-boxの1行で解決しました。
どちらも、計算のルールを知らないと「CSSがおかしい」で終わり、知っていれば、開発者ツールで計算された値を一度見れば済みます。
次のクイズで確認すること
box-sizingの2つのモードでボックスが実際に占める幅、マージンの相殺が起きる3つのケースと止める方法、そして、パーセンテージが何を基準に測るのかを確認します。