BeAct Co., Ltd.

BLOG
社員ブログ

margin を指定したのに消えた!? ~CSS のマージン相殺を理解しよう~

margin を指定した要素を並べてプレビューしてみたとき、「なんだか思ったレイアウトと違うな」と感じたことはありませんか? それはひょっとしたら、「マージンの相殺」というルールによって発生しているのかもしれません。

とくに初心者の方は、この挙動はわかりにくいと思いますので、こんかいはこのルールについて解説します。


最初に結論

  • ブロック要素の並びや入れ子で、
  • それぞれに上や下のマージンが指定され、マージン同士が接触した場合、
  • 互いのマージンは合算されず、絶対値の大きい方のマージンが採用される。
    • ただし、正と負のマージンが接触した場合に限り、両者は合算される

ブロック方向のマージン指定について

ブロック要素同士を並べると、それは縦方向(厳密には「そのページを読み進める方向」)に積み重なります。何も指定がない場合、隙間なくくっついて積み重なりますが、見やすさのために余白を空けたいというケースは頻繁に出てきます。この「余白」を表現するのが、CSS における「上下マージン指定」、例えば margin-block: 20px;margin-block: 20px 40px; といったものです。

具体的には、以下のコード例をご覧ください。

内側のブロック要素の上下方向の余白を確認してみると、それぞれに指定した値が適用されていることが確認できますね。

指定したマージンが消えた?!

さて、この内側のブロック要素が複数になり、「積み重なった」場合、余白はどのように空くのでしょうか? 実際に見てみましょう。

……おかしいですね。上下方向にマージンを設定しているはずなのに、まるで2番めの要素の上方向のマージンが設定されていないかのように見えています。もちろん、特別なプロパティやスタイル指定などをしているわけではありません。

さらに、上下のマージンの大きさを逆にしてみましょう。

こんどは、まるで最初の要素の下方向のマージンが設定されていないかのように見えています!

しかし、これは CSS が壊れているわけではありません。CSS の仕様のひとつ、すなわちマージンの相殺 (margin collapsing) が働いた結果なのです。

マージンの相殺とは

マージンの相殺とは、

(原則として)通常フローにあるブロック要素同士では、上下方向のマージンが接触すると、それらは合算されず、(正の値同士なら)もっとも大きい値が採用される。

ただし、正負のマージン同士の接触に限り、合算される

というルールです。しかしそもそも、どうしてこのような直感に反するかのようなルールができたのでしょうか?

なぜ相殺というルールがあるの?

HTML や CSS は、もともと文書レイアウトを強く意識して設計されました。特に段落(<p>)を上下方向に積み重ねるモデルは、紙の文書レイアウトとも相性が良かったと考えられます。

しかし、当時の設計思想を想像すると、次のような問題があったことが予想できそうです。

/** 「段落」の間の余白は、40px にしたいけど… */
p {
  margin-block: 40px; /** これじゃ、段落の「上と下」に、40px の余白ができるのでは? */
}

/** じゃあ、これらのどちらかにすればいいのか? */
p {
  margin-top: 40px; /** これじゃ、「最後」の段落の「下」に、余白が空かない! */
}
p {
  margin-bottom: 40px; /** これじゃ、「最初」の段落の「上」に、余白が空かない! */
}

/** じゃあ、こうなのか? */
p {
  margin-top: 20px;
  margin-bottom: 20px;
  /** 「40px」という表明が、コードから読み取れない! */
}

単に「段落間の余白」を定義したいだけなのに、その表現は想像以上に複雑なものでした。その結果として採用されたのが、以下のマージン相殺の仕組みです。

/** 「段落間の余白を 40px にする」という意図を、こう表現できる。 */
p {
  margin-block: 40px;
}

先の、「これじゃ、段落の『上と下』に、40px の余白ができるのでは?」の書き方と同じですね。しかしここでは、「各段落が、自分の上下に 40px の余白を主張しても、実際の表示ではブラウザ側でうまく調整してほしい」という考え方を表現したものとなっています。

つまり CSS は、「余白」を親やレイアウト全体が管理するのではなく、「各要素が自分自身の余白を主張する」という設計を選びました。

しかしその結果、隣接する要素同士で「余白」が衝突するケースが発生します。そこでブラウザ側が、それらをひとつの余白として整理するルールを導入しました。

そのルールこそが、「マージンの相殺」です。

現在では gap によるレイアウト制御も普及しましたが、通常フローの文書レイアウトでは、マージンの相殺は依然として重要な仕様です。

マージンは「どこで」相殺されるのか?

それでは、さらに紐解いてみましょう。ここで大切になる概念が「マージンの接触」です。

兄弟要素間でのマージンの相殺

まずは、もっとも基本的な「兄弟要素間」でのマージンの相殺を見てみましょう。

以下のように、上下に積み重なった 2 つのブロック要素を考えます。

このとき、

  • 最初の要素は margin-bottom
  • 次の要素は margin-top

を持っています。しかし実際に表示される余白は、それらを合算した値にはなりません。なぜなら、これらのマージンは「接触している」からです。

CSS の通常フローでは、上下に積み重なったブロック要素同士のマージンが接触すると、ブラウザはそれらを別々の余白として扱わず、「ひとつの余白」として整理します。これが、もっとも基本的なマージンの相殺です。

例えば、

.first {
  margin-bottom: 40px;
}

.second {
  margin-top: 60px;
}

であれば、実際の余白は 40px60px を合算した 100px ではなく、両者のうちでより大きい 60px となります。

つまりブラウザは、

  • 「40px の余白」
  • 「60px の余白」

を別々に並べるのではなく、

この場所には、最大 60px の余白が必要なのだな。

と解釈しているわけです。これは前の章で説明した、

各要素が、自分の余白を主張する。

という考え方の、もっとも単純な例と言えるでしょう。

親子要素間でのマージンの相殺

マージンの相殺は、兄弟要素間だけで発生するわけではありません。実は、「親子要素間」でも発生します。

以下のコードを見てください。緑色が親要素ピンク色が子要素です。

内側の子要素には margin-top が指定されています。しかし実際に表示してみると、親要素自身の margin-top10px のままですが、見た目はそれ以上に押し下げられているように見えます。

これは、

  • 親要素の上端
  • 最初の子要素の margin-top

が接触し、マージンの相殺が発生しているためです。言い換えると、子要素が持っているはずの上方向のマージンが、「親の外側」に飛び出したように見えているのです。

同じことは、

  • 親要素の下端
  • 最後の子要素の margin-bottom

でも発生します。次の例では、親要素の下方向のマージンは 30px なのに、子要素80px のマージンが接触しているため、相殺が発生し、こうなってしまうのです。

そのため、CSS を書いていると、

子要素に margin を指定したのに、なぜか親要素ごと動いてしまった!

という現象に遭遇することがあります。これはバグではなく、マージンの相殺によるものです。ここは特に初心者が混乱しやすいポイントなので、覚えておくとよいでしょう。

マージンの相殺が発生しない条件

ここまで読むと、

上下のマージンは、常に相殺されるのか?

と思うかもしれません。しかし実際には、マージンの相殺が発生しないケースも多く存在します。

以下、そういったケースを紹介します。

マージン同士の接触が「膜」で阻まれている場合

例えば、親要素に以下のようなスタイル指定をすると、親子間のマージンは接触できなくなります

  • padding(要素の内のり)
  • border(要素の線)
  • など。

その理由は、親要素にこれらのスタイルを指定すると、そのすぐ内側に「『膜』のようなもの」ができるためです(もちろん実際の CSS 用語ではなく、イメージとしてです)。このは、

  • 親要素の外側
  • 子要素のマージン

間に割り込むことにより、互いの接触を防ぎます。その結果、相殺が起こらなくなる……というイメージです。親要素にわずかでも padding-topborder-top を指定すると、親子のマージンはこのに阻まれ接触できなくなるため、この相殺は発生しなくなります。

同様の理由で、親要素の先頭や末尾に、(テキストなどの)インラインコンテンツが存在する場合も、それがとなって、そこでの相殺が起こらなくなります。

通常のフローレイアウトではない場合

そもそも通常フローではないレイアウト、例えば:

  • float 要素
  • 絶対配置要素 (position: absolute)
  • 固定配置要素 (position: fixed)

などでは、マージンの相殺は発生しません。

さらに、以下のようにブロック整形コンテキスト(BFC)を形成する場合も、マージンの相殺は抑制されます。

  • overflow: hidden
  • overflow: auto
  • display: flow-root

加えて、以下のように Flexbox や Grid Layout を指定した場合も、マージンの相殺は抑制されます。

  • display: flex
  • display: grid

この仕様のため、display: block では発生していた余白の問題が、display: flex に変更した途端に解消される、というケースも実務ではよくあります。現代の CSS では Flexbox や Grid Layout を使う機会も多いため、

通常フローでは相殺が起きる。

Flexbox や Grid では起きない。

という違いを意識しておくと、レイアウトのトラブルシューティングがしやすくなります。

マージンの相殺で、適用される優先順位

ここまで、「絶対値の大きい方のマージンが採用される、正と負なら合算される」と説明してきました。この意味を掘り下げて説明します。

正どうし、または負どうしのマージンの場合

まず、正の値同士であれば、もっとも大きい値が採用されます。例えば:

.a {
  margin-bottom: 20px;
}

.b {
  margin-top: 50px;
}

であれば、結果は 50px になります。

一方で、負の値(ネガティブマージン)同士であれば、「もっとも小さい値(絶対値の大きい負の値)」が採用されます。例えば:

.a {
  margin-bottom: -50px;
}

.b {
  margin-top: -20px;
}

であれば、結果は -50px になります。

絶対値の大きい方が採用される」とは、このことです。

正と負のマージンの場合

ところが、正と負のマージンが含まれる場合だけは、例外的に “絶対値の大きい方” ではなく、合算となります。例えば:

.a {
  margin-bottom: 50px;
}

.b {
  margin-top: -20px;
}

では、50px-20px が合成され、結果は 30px になります。

まとめ

CSS におけるマージンの相殺とは、

  • 各要素が持つマージン同士が接触したとき、
  • ブラウザがそれらをひとつのマージンとして整理する、

という仕組みです。

そのため、

  • 兄弟要素間
  • 親子要素間

では、意図しないマージンの移動や消失が起こったように見えることがあります。しかしこれはバグではなく、CSS が「余白(マージン)」を自然に扱うために導入した仕様です。

また、

  • padding
  • border
  • Flexbox
  • Grid Layout

などによって接触が遮られると、マージンの相殺は発生しません。

レイアウトが思った通りにならない時は、

このマージンは、どのマージンと接触しているのか?

を意識すると、原因を見つけやすくなるでしょう。

こんかいのお話は、以上です。それでは、よきコーディングライフを!