CSS で margin-bottom: 40px と margin-top: 20px を隣り合う要素に指定したのに、実際のすき間が 60px にならない。あるいは、子要素に margin-top を付けたら親要素ごと下にずれてしまった。これらはどちらも「マージンの相殺(margin collapsing)」という CSS の仕様によるものです。この記事では、マージンの相殺がどういう現象なのか、どんな条件で起きて、どんなときには起きないのかを整理し、実務で使える防ぎ方までを解説します。実際に見た目の差が分かるデモも用意しました。
目次
マージンの相殺(margin collapsing)とは
マージンの相殺とは、上下に接するブロック要素の縦方向のマージンが合算されず、大きいほうだけが適用される現象です。たとえば上の要素に margin-bottom: 40px、下の要素に margin-top: 20px を指定した場合、2つの要素のすき間は合計の 60px ではなく、大きいほうの 40px になります。小さいほうの 20px は大きいほうに吸収されて消える、と考えると分かりやすいです。
.box-a {
margin-bottom: 40px;
}
.box-b {
margin-top: 20px;
}
/* 実際のすき間は 40px + 20px = 60px ではなく、大きいほうの 40px */
これはバグではなく、CSS の仕様として定められた挙動です。文章のように縦へ積み重なるコンテンツで、段落と段落のすき間が指定した値の2倍になってしまうのを避けるための仕組みだと考えると、意図が理解しやすくなります。逆に言えば、カード型のレイアウトやコンポーネント単位で余白を設計するときには「足されるはずの余白が消える」ように見えて、意図しない結果になりがちです。
相殺が起きる3つのパターン
相殺は、マージン同士が間に何も挟まずに直接触れ合っているときに起こります。触れ合い方には3つのパターンがあり、それぞれ見え方が違います。
隣接する兄弟要素の下マージンと上マージン
もっとも分かりやすいのがこのパターンです。縦に並んだ兄弟要素で、上の要素の margin-bottom と下の要素の margin-top が接すると相殺し、大きいほうの値だけがすき間になります。段落や見出しが縦に並ぶ通常の文書では、常にこの相殺が起きています。
<div class="box-a">上の箱</div>
<!-- この2つの間は 40px -->
<div class="box-b">下の箱</div>
親と最初・最後の子の間
親要素と、その最初の子要素の margin-top の間でも相殺は起こります。親に border-top・padding-top・インラインコンテンツ(テキストなど)のどれも無いと、親の上端と子の上端が直接接することになり、子の margin-top が親の外側へ飛び出します。結果として、親の内側に余白ができるのではなく、親要素そのものが下へ押し下げられたように見えます。
/* 親に border も padding も無い */
.parent {
background: #fde68a;
}
/* この 32px は親の内側の余白にならず、親の外へ出てしまう */
.child {
margin-top: 32px;
}
同じことが下側でも起こります。親に border-bottom・padding-bottom・インラインコンテンツが無く、さらに親の高さが auto(height を指定していない)のとき、最後の子の margin-bottom が親の margin-bottom と相殺して外へ出ます。「子に余白を付けたのに親の背景が広がらない」という現象の正体は、たいていこれです。
中身が空の要素の上下マージン同士
3つ目は少し特殊で、要素自身の margin-top と margin-bottom が相殺するパターンです。要素に height・min-height・border・padding・中身のコンテンツのいずれも無いと、その要素の上端と下端が同じ位置になるため、自分の上下マージンが直接触れ合って1つにまとまります。
/* 中身が空の <div class="spacer"></div> */
.spacer {
margin-top: 24px;
margin-bottom: 16px;
}
/* 24px + 16px = 40px ではなく、占める高さは 24px になる */
さらに、この空要素の前後にも要素があると、前の要素の margin-bottom・空要素の上下マージン・次の要素の margin-top がすべて連鎖して1つにまとまり、そのなかで最大の値だけが残ります。マージンだけで高さを稼ごうとした空の <div> が期待どおりに効かないのは、この連鎖が原因です。高さが必要なら height や padding を与えるほうが確実です。
兄弟要素の相殺を目で確かめる
下のデモは、上の箱に margin-bottom: 40px、下の箱に margin-top: 20px を指定したものです。水色の部分がマージンによるすき間で、その高さは 60px ではなく 40px になっています。CSS タブで .box--a の margin-bottom を 10px に変えてみてください。今度は下の箱の margin-top: 20px のほうが大きくなるので、すき間は 20px に変わります。常に大きいほうが勝つことが確かめられます。
<div class="demo">
<div class="box box--a">上の箱:margin-bottom: 40px</div>
<div class="box box--b">下の箱:margin-top: 20px</div>
</div>
<p class="note">2つの箱のすき間は 40px + 20px = 60px ではなく、大きいほうの 40px になります。</p>
body {
margin: 0;
padding: 16px;
font-family: sans-serif;
background: #f8fafc;
color: #1e293b;
}
/* 親に padding を入れて、親子間の相殺は起きない状態にしています。
水色の部分がマージンによるすき間です。 */
.demo {
padding: 16px;
background: #e0e7ff;
border: 1px solid #a5b4fc;
border-radius: 8px;
}
.box {
padding: 12px;
background: #fff;
border: 1px solid #6366f1;
border-radius: 4px;
text-align: center;
font-size: 14px;
}
/* 下の箱の margin-top と相殺し、実際のすき間は 40px になります */
.box--a {
margin-bottom: 40px;
}
.box--b {
margin-top: 20px;
}
.note {
margin: 12px 0 0;
font-size: 13px;
color: #475569;
}
相殺が起きない条件
相殺は「通常フローに並ぶブロック要素の、縦方向のマージン」に限って起こります。裏を返せば、条件から外れる場面ではまったく起こりません。まず押さえておきたいのは、横(左右)方向のマージンは相殺しないことです。margin-right: 40px の要素と margin-left: 20px の要素が横に並べば、すき間はそのまま合計の 60px になります。相殺は縦方向だけの話です。
そのほか、レイアウトの組み方や配置方法によっても相殺は起きなくなります。主なものを表にまとめます。
| 状況 | 相殺の起き方 |
|---|---|
| 左右方向のマージン | 起きない。値はそのまま足される |
display: flex / display: grid のアイテム同士 | 起きない。通常フローで並んでいないため |
position: absolute / position: fixed の要素 | 起きない。通常フローから外れるため |
float を指定した要素 | 起きない(float を解除しない限り) |
overflow が hidden / auto / scroll の要素 | その内側と外側の間では起きない |
display: flow-root の要素 | その内側と外側の間では起きない |
ルート要素(<html>)のマージン | 起きない |
表の overflow と flow-root に共通しているのは、新しいブロック整形コンテキスト(BFC: Block Formatting Context)を作るという点です。BFC はレイアウト計算の独立した箱のようなもので、その中と外はマージンの計算上つながりません。そのため親と子の間の相殺が止まります。ただし BFC はあくまで内と外を隔てるものなので、同じ BFC の内側にある兄弟同士の相殺は止まりません。この違いは、防ぎ方を選ぶときに効いてきます。
Flexbox と Grid が特別なのは、BFC の話ではなくそもそも子要素が通常フローで並んでいない点です。フレックスアイテム・グリッドアイテム同士のマージンは、縦方向であっても一切相殺しません。上のアイテムの margin-bottom: 40px と下のアイテムの margin-top: 20px は、素直に 60px のすき間になります。
負のマージンが混ざったときの計算
相殺は「大きいほうが勝つ」と説明しましたが、これは正の値だけのときの話です。負のマージンが混ざると計算方法が変わります。
相殺する値がすべて正なら、そのなかの最大値が採用されます。すべて負なら、もっとも小さい値(絶対値がもっとも大きい負の値)が採用されます。そして正と負が混ざっている場合は、正のなかの最大値と、負のなかの最小値を足した結果になります。
/* すべて正: 40px と 20px なら、大きいほうの 40px */
/* すべて負: -30px と -10px なら、小さいほうの -30px */
/* 混在: 40px と -10px なら、40 + (-10) = 30px */
.box-a {
margin-bottom: 40px;
}
.box-b {
margin-top: -10px; /* すき間は 30px になる */
}
負のマージンで要素を重ねるテクニックを使うとき、相殺を意識していないと「思ったほど重ならない」「逆に想定以上に食い込む」といったズレが出ます。重なりを厳密に制御したい場面では、次に説明する方法で相殺そのものを止めるか、Flexbox や Grid のように相殺が起きない土台の上で調整するほうが確実です。
マージンの相殺を防ぐ方法
相殺を止める手段はいくつかありますが、どれも見た目やレイアウトへの副作用があります。「親子の相殺を止めたいのか、兄弟の相殺を止めたいのか」で選び分けるのがポイントです。
親に padding か border を入れる
親子間の相殺は、親の端と子の端が直接接していることで起こります。そのため親に padding-top: 1px や border-top: 1px solid transparent を入れて間に何かを挟めば、接触がなくなって相殺は止まります。もっとも手軽な反面、その1pxぶん要素の高さが増えます。デザイン上もともと親に padding を持たせるカードのような要素なら自然な解決ですが、相殺を止めるためだけに透明な border を入れるのは意図が伝わりにくく、後から「不要な指定」と判断されて消されやすいコードになります。
display: flow-root を使う
親子間の相殺を止める目的なら、もっとも素直なのが display: flow-root です。この値は新しいブロック整形コンテキストを作ることだけを目的に用意されたもので、余計な副作用がありません。親に指定すれば、子のマージンが外へ飛び出さなくなります。同じく BFC を作る overflow: hidden でも相殺は止まりますが、そちらははみ出したコンテンツが切り取られるという本来の効果がついてくるため、ドロップシャドウやツールチップが見えなくなる事故が起こります。相殺を止めたいだけなら flow-root を選びましょう。
/* 子のマージンを親の内側に閉じ込める */
.parent {
display: flow-root;
}
下のデモで、左右の違いを見比べてみてください。左は通常の div なので、子の margin-top: 32px が親(黄色い背景)の外へ飛び出し、親の背景が子にぴったり張り付いています。右は親に display: flow-root を指定したもので、32px が親の内側の余白として残り、黄色い背景が子の上まで広がっています。CSS タブで .parent--flow-root の display: flow-root; を消すと、右も左と同じ見た目になります。
<div class="row">
<div class="col">
<p class="label">通常(相殺が起きる)</p>
<div class="parent">
<div class="child">子:margin-top 32px</div>
</div>
</div>
<div class="col">
<p class="label">display: flow-root</p>
<div class="parent parent--flow-root">
<div class="child">子:margin-top 32px</div>
</div>
</div>
</div>
body {
margin: 0;
padding: 16px;
font-family: sans-serif;
background: #f8fafc;
color: #1e293b;
}
.row {
display: flex;
gap: 16px;
}
.col {
flex: 1;
padding: 12px;
background: #fff;
border: 1px solid #cbd5e1;
border-radius: 8px;
}
.label {
margin: 0 0 8px;
font-size: 13px;
font-weight: bold;
color: #334155;
}
/* 親に border も padding も無いので、子の margin-top が親の外へ飛び出します。
黄色い親の背景が子にぴったり接しているのが目印です。 */
.parent {
background: #fde68a;
border-radius: 4px;
}
/* flow-root にすると親の中に独立したブロック整形コンテキストができ、
子の margin-top が親の内側の余白として残ります。 */
.parent--flow-root {
display: flow-root;
}
.child {
margin-top: 32px;
padding: 10px;
background: #f59e0b;
color: #fff;
border-radius: 4px;
font-size: 13px;
text-align: center;
}
Flexbox や Grid に切り替える
親を display: flex や display: grid にすると、子は通常フローから外れるため、親子間だけでなく兄弟同士の相殺もまとめて止まります。相殺を根本からなくせるという点では一番強力な方法です。縦並びのままにしたい場合は flex-direction: column を添えます。ただし、フレックスコンテナにすると子の幅の伸縮やベースラインの揃い方など、余白以外の挙動も変わります。既存のレイアウトに後付けすると別の崩れを呼び込むことがあるので、レイアウトを設計する段階から選ぶのが安全です。要素間の余白そのものは、マージンではなく gap で指定すると計算が単純になります。
/* 縦並びのまま、相殺しないレイアウトにする */
.stack {
display: flex;
flex-direction: column;
gap: 24px; /* すき間は gap で一括管理する */
}
マージンを片方向だけに統一する
相殺を止めるのではなく、そもそも相殺が問題にならない書き方に揃えるのも実務では有効です。プロジェクト全体で「縦の余白は margin-bottom だけで付ける」と決めてしまえば、隣り合う要素のうち片方のマージンは常に 0 になるため、相殺しても結果は変わりません。上の要素の 40px と下の要素の 0 が相殺すれば 40px、そのまま指定どおりです。
この方針の利点は、CSS を読むときに「このすき間はどちらの要素が作っているのか」を迷わなくて済むことです。欠点は、ブラウザの既定スタイルやリセット CSS、他人が書いたコンポーネントが margin-top を持っていると崩れる点で、ルールを守り切るにはリセット CSS 側での初期化も必要になります。とはいえ規約として明文化しやすく、Flexbox への切り替えのようにレイアウトを変えずに済むため、既存プロジェクトにも導入しやすい方法です。
子要素の margin-top が親の外に出てしまうとき
実務でつまずくことが多いのは、圧倒的に親子間の相殺です。症状ごとに、確認するポイントを見ていきます。
親の背景色が子の余白まで広がらない
親に背景色を付けたのに、最初の子に指定した margin-top のぶんだけ背景が塗られていない、という状態です。これは子のマージンが親の外へ出て、親そのものを押し下げているために起こります。親の上端は子の上端と同じ位置にあるので、余白の部分は親の外側、つまり背景の対象外というわけです。
子の余白を親の内側に入れたいなら、親に display: flow-root を指定するか、親の padding-top として余白を持たせて子の margin-top を 0 にします。背景色を持つ親は padding でデザインすると考え方が一貫し、相殺も自然に起きなくなります。
要素の間隔が指定した値と合わない
「上に 24px、下に 16px」と指定したのに、実際は 24px しか空いていない。これは兄弟同士の相殺で、小さいほうが吸収された典型例です。ブラウザの開発者ツールで要素を選ぶと、実際に適用されているマージンの範囲が色付きで表示されるので、そこで指定値と実際の余白のどちらがどう効いているかを確認できます。
この場合、親を BFC にしても兄弟同士の相殺は止まらない点に注意してください。display: flow-root は内と外を隔てるだけで、中の要素同士は依然として同じ通常フローに並んでいます。兄弟の相殺を止めたいなら、親を Flexbox や Grid にして gap で余白を指定するか、片方向のマージンに統一するのが現実的な対処です。
ページ全体が下にずれる
ページ先頭の見出しに付けた margin-top が、ラッパー要素を突き抜けて <body> まで伝わり、ページ全体が下にずれることがあります。相殺は親をたどって連鎖するので、border も padding も持たないラッパーが何重にも入れ子になっていると、一番内側の子のマージンが最外側まで届いてしまうためです。
どこで止まっているかを調べるには、開発者ツールで <body> から順に要素を選び、マージンの表示が付いている要素を探します。原因の要素が分かったら、そのラッパーに display: flow-root を指定するか、先頭の要素の margin-top をやめて親の padding-top に置き換えます。ページの外枠に近いラッパーほど、余白は padding で持たせておくと安定します。
まとめ
マージンの相殺は、上下に接するブロック要素の縦マージンが合算されず、大きいほうだけが適用される仕様です。margin-bottom: 40px と margin-top: 20px なら 60px ではなく 40px になります。起きるのは隣接する兄弟の間、親と最初・最後の子の間、そして中身が空の要素の上下マージン同士の3つのパターンで、負の値が混ざるときは正の最大値と負の最小値を足した結果になります。横方向のマージン、Flexbox・Grid のアイテム、position: absolute や float の要素、そして BFC の内と外では相殺しません。
防ぎたいときは、親子間なら display: flow-root か親の padding、兄弟間も含めて根本から避けたいなら Flexbox や Grid の gap、既存プロジェクトに後付けするならマージンを片方向に統一する、という使い分けが実用的です。仕様として知っておけば「余白が消える」現象に驚かされることはなくなり、余白の設計そのものを見直すきっかけにもできます。