メインコンテンツへ移動

マージンの相殺ルール

無料2017-11-12#CSS#CSS margin collapse#collapsed margin#合并外边距#外边距折叠#CSS clearance

CSSボックスモデルにおいて、非常に興味深いながらも理解するのが難しい内容の一つです。

はじめに

マージン(margin)の相殺ルールは、CSSボックスモデルの中で最も複雑な部分の一つと言っても過言ではありません。なぜなら、この部分にはクリアランス(clearance)、通常のフロー/インフロー(normal flow/in-flow)、BFC(ブロック整形文脈)、行ボックス(line box)、インラインボックス(inline box)、bidi(双方向環境)など、理解しにくい概念が多く関わっているからです。

CSSボックスモデルは、単に7つの水平プロパティ + 7つの垂直プロパティだけではありません:

margin
  border
    padding
      width/height

P.S. ハイヒールのジョークを思い出します——「paddingだけでなく、今日はmarginも追加したの」

関連する内容には、少なくとも以下が含まれます:

  • context-boxborder-box (※原文は context-box ですが、おそらく content-box の意)

  • padding/margin のパーセンテージの計算方法

  • backgroundpadding/margin/border の関係

  • margin の負の値

  • margin の相殺

ボックスモデルは視覚整形モデルにおける基礎単位であり、CSSレイアウトモデルにおいて不可欠な要素です。

CSSボックスモデルは、ドキュメントツリー内の要素に対して生成され、視覚整形モデルに従って配置される矩形ボックスを記述します。

8 ボックスモデル より引用)

したがって、ボックスモデルはCSSがドキュメントツリーの上に構築した第一層の抽象化であり、CSSレイアウト制御がドキュメント要素と直接関連付けられる部分です。そして、マージンの相殺は垂直方向の整形に直接影響を与える要因の一つであり、深く理解しておく必要があります。

1. 典型的なシナリオ

以下の例では、ユーザーエージェント(UA)にデフォルトのスタイルシートがなく、宣言されていないスタイルプロパティはすべて仕様に従って初期値をとると仮定します。

また、UAはすべてCSS仕様を遵守しているものとします。

1. リスト項目間のマージン相殺

li {
    margin: 8px;
}

このとき、リスト項目間の間隔はいくらになるでしょうか?

.li-case1 li {
    margin: 8px;
    /* 上パディングを追加 */
    padding-top: 1px;
}

.li-case2 li {
    margin: 8px;
    /* 下ボーダーを追加 */
    border-bottom: 1px solid;
}

case1 と case2 において、リスト項目間の間隔はそれぞれいくらになるでしょうか?

2. 深い入れ子構造でのマージン相殺

/* インデントはドキュメント構造の入れ子関係を表します */
div.outer,
  div.container,
    div.content,
      div.inner {
    margin: 10px;
    min-width: 100px;
    min-height: 100px;
}

これら4つの入れ子になった div のレンダリング結果はどうなるでしょうか?

div.outer,
  div.container,
    div.content,
      div.inner {
    margin: 10px;
    min-width: 100px;
    min-height: 100px;
    /* ボーダーを追加 */
    border: 1px solid;
}

今度はどうでしょうか?

div.outer,
  div.container,
    div.content,
      div.inner {
    margin: 10px;
    /* min-width, min-height, border を削除 */
}

では、この場合は?

3. クリアランスを伴うマージン相殺

div.container {
    border-top: 1px solid;
    background: #ccc;
    margin-bottom: 60px;
}
  /* インデントはドキュメント構造の入れ子関係を表します */
  div.float {
      float: left;
      width: 100px;
      height: 50px;
  }
  div.following-float {
      clear: left;
      margin-top: 50px;
  }
div.following-container {
    color: red;
}

赤いテキストの上端から .following-float の下端までの距離はいくらでしょうか?

div.container {
    border-top: 1px solid;
    background: #ccc;
    margin-bottom: 60px;
}
  /* インデントはドキュメント構造の入れ子関係を表します */
  div.float {
      float: left;
      width: 100px;
      height: 50px;
  }
  div.following-float {
      clear: left;
      /* 50を49に変更 */
      margin-top: 49px;
  }
div.following-container {
    color: red;
}

今度はどうでしょうか?

さらに 50051 に変えた場合は? それぞれどのような状況になるでしょうか?

P.S. これらの問題の答えは現時点では未知です。なぜならデモをまだ書いていないからです ;-) ですから、真剣に予想する時間は十分にあります。

2. 相殺の条件

どのようなマージンが相殺されるのでしょうか?

水平マージンは相殺されません。隣接する垂直マージンは相殺されます。ただし、以下の2つの特殊な場合を除きます:

  • ルート要素のボックスのマージンは相殺されない

  • クリアランスを持つ要素の上マージンと下マージンが隣接している場合、そのマージンは直後の兄弟要素の隣接するマージンと相殺されますが、相殺後の結果は親ブロックの下マージンとは相殺されません。

第1項目は省略します。ルート要素にマージンを適用することは通常ありません。

第2項目では「クリアランス(clearance)」という新しい概念が登場しました。名前からして clear プロパティに関連しているようですが、直感通り、clear プロパティによって要素の位置が移動した際に生じる間隙を指します(CSS仕様 9 視覚整形モデル を参照)。これには2つの重要なポイントが含まれます:

  • clear プロパティが適用されていること

  • かつ(clear プロパティによって)要素の位置が移動していること

これら2つの条件を満たす場合、その要素はクリアランスを持つと言います。

注意clear プロパティを適用しても要素の実際の位置が変わらない場合(例えば、margin-top によってすでにその位置に配置されており、clear の効果による位置と同じである場合)、クリアランスは持ちません。逆に、clear プロパ���ィを適用したことで要素の実際の位置が変化し、要素の上に clear プロパティによって生じた空間がある場合、それはクリアランスを持つことになります。

クリアランスを持つだけでは不十分で、その要素の上マージンと下マージンが隣接している(つまり、要素の実際の高さが0で、paddingborder がない)必要があります。これらを同時に満たす場合、その要素のマージン相殺は制限されます。つまり、そのマージンは直後の兄弟の隣接マージンとのみ相殺され、その結果は親ブロックの下マージンとは相殺されません。

P.S. ここでようやく典型的なシナリオ3に挑戦する準備が整いましたが、道のりはまだ遠いです。

「隣接」の定義

2つのマージンがどのような場合に「隣接」しているとみなされるのでしょうか?

  • どちらも通常のフロー内(in-flow)にあるブロックレベルボックスであり、同じブロック整形文脈(BFC)に属していること

  • 行ボックス(line box)、クリアランス、パディング、ボーダーによってそれらが隔てられていないこと

  • どちらも垂直方向に隣接するボックスの境界(vertically-adjacent box edges)に属していること

3つの文章に4つの新しい概念が含まれています。順番に見ていきましょう。

通常のフロー内(in-flow)

通常のフロー内/フロー外(in-flow/out-of-flow)とは、その要素の配置に通常のフロー配置スキームが使われているかどうかを指します。

配置スキームは3つの種類に分けられます:

  • 通常のフロー。ブロック整形、インライン整形、相対配置を含みます。

  • フロート。通常のフローの位置から取り出され、左または右に移動します。

  • 絶対配置。通常のフローから脱離し、包含ブロックに基づいて自身の位置を決定します。

要素がフロートしておらず(float プロパティが none)、絶対配置もされておらず(position プロパティが absolute または fixed ではない)、ルート要素でもない場合、それは通常のフローに従って配置され、*通常のフロー内要素(in-flow)に属します。そうでなければフロー外要素(out-of-flow)*です。

ブロック整形文脈(BFC)

フロート、絶対配置された要素、ブロックボックスではないブロックコンテナ(inline-block、table-cell、table-caption など)、および 'overflow' が 'visible' ではないブロックボックス(その値がビューポートに伝播された場合を除く)は、その内容のために新しいブロック整形文脈を確立します。

ブロック整形文脈内では、ボックスは包含ブロックの最上部から始めて、垂直方向に一つずつ配置されます。2つの兄弟ボックス間の垂直方向の距離は 'margin' プロパティによって決定されます。

つまり、誰も新しいBFCを確立していなければ、現在のBFCに属します。JavaScriptのスコープのように、デフォルトでは全員が最も外側のスコープ(最も外側のブロック整形文脈)に位置し、普通のブロックレベルボックスに出会えばブロック整形文脈に入れられ、特殊なもの(フロート、絶対配置など)に出会えば新しいスコープの層(新しいブロック整形文脈)が作られ、その中の要素はその内側のスコープ(新しいブロック整形文脈)に入れられます。

配置が完了した後、整形文脈の観点から見ると、それは入れ子になった一連のBFCであり、各BFCが一連のブロックボックス(またはブロックレベル要素)の配置を管理していることになります。

注意:ここではインライン整形文脈については触れません。なぜなら、異なるインライン整形文脈を区別することに大きな意味はないからです(仕様の定義において、インライン整形文脈をまたぐ特殊なシナリオはありません)。では、いつ新しいインライン整形文脈が作られるのでしょうか? 仕様によれば、ブロックコンテナがインラインレベルのボックスのみを含む場合にのみ、新しいインライン整形文脈が作成されます。BFCのように明示的に強制作成することはできません。

P.S. 新しいインライン整形文脈がいつ作成されるかについての更なる議論は、When does a box establish an inline formatting context? を参照してください。

行ボックス(line box)

同じ行に属するボックスを含む矩形領域を行ボックスと呼びます。

行ボックスは、そこに含まれるすべてのボックスを収容できるだけの十分な高さを持っています。

行ボックスはCSSにおける「行」の抽象表現であり、各行の要素は同じ行ボックス内に存在します。要素が長すぎて入りきらずに自動改行が発生する場合、次の行のために新たな行ボックスが作成されます。一方で、行ボックスは単なる抽象的な定義ではなく、幅と高さを持ち、行のレイアウトを決定します。

隣接するマージンの間に「行ボックスがない」というのは、簡単に言えば、それらを隔てるインライン要素が存在しないことを意味します。

垂直方向に隣接するボックスの境界

以下の4つのシナリオにおいて、マージンが垂直方向に隣接するボックスの境界に属するという条件を満たします:

  • ボックスの上マージんと、その最初の通常のフロー内(in-flow)の子の上マージン

  • ボックスの下マージんと、その直後の通常のフロー内兄弟の上マージン

  • 最後の通常のフロー内の子の下マージんと、heightの算出値が 'auto' である親要素の下マージン

  • ボックスの上マージンと下マージン。ただし、そのボックスが新しいブロック整形文脈を確立しておらず、かつ 'min-height' の算出値が0、'height' の算出値が0または 'auto' であり、かつ通常のフロー内の子を持っていないことが条件です。

少し長く見えるので、すべてが通常のフロー内要素であると仮定して簡略化すると:

  • 親子:親要素の上マージンと長男の上マージン

  • 兄弟:要素の下マージンと右隣の兄弟の上マージン

  • 親子:末っ子の下マージンと親要素の下マージン

  • 自身:高さ0の「真空」要素の上マージンと下マージン

P.S. ここでの「真空」とは、ポテトチップスの袋を真空にするようなイメージです。中身が何もないか、通常のフロー内の子がすべてフロー外に追い出されている状態を指します。

つまり、「隣接するマージン」の位置定義は、親子、兄弟、そして自身(自身の上下マージンの相殺は少し特異です)の3つのケースに分けられます。

「隣接」とマージンの相殺を再理解する

これまでの概念を踏まえて、バラバラだった���を統合し、「隣接」を再定義しましょう:

親子、兄弟、または要素自身のマージンがぴったりくっついていることが「隣接」です。

もう一つの重要なポイントは、ぴったりくっついているということです。つまり、これら2つのマージンの間に「壁」がないということです。「壁」には3つの種類があります:

  • 属性:双方が通常のフロー内のブロックレベルボックスであること

  • 所属:同じブロック整形文脈(BFC)に属していること

  • 物理的障壁:二者の間に行ボックス(line box)、クリアランス、パディング、ボーダーが存在しないこと

これで「隣接」が明確になりました。マージンの相殺の定義を改めて整理すると:

ルート要素以外の隣接する垂直マージンは相殺される。ただしクリアランスがある場合は相殺が制限される。

制限されるとは、クリアランスを持つ要素自身の上下マージンが隣接している場合、それは兄弟要素のマージンとのみ相殺でき、親要素の下マージンとは相殺できないことを指します。

3. 相殺条件の推論

マージンの相殺が発生する条件に基づき、8つの推論が導き出されます:

  • フロートされたボックスと他のボックスとの間のマージンは相殺されない(フロートボックスとその通常のフロー内の子との間でも同様)。

  • 新しいブロック整形文脈(BFC)を確立した要素(例:フロートボックスや 'overflow' が 'visible' ではない要素)のマージンは、その通常のフロー内の子と相殺されない。

  • 絶対配置されたボックスのマージンは相殺されない(その通常のフロー内の子との間でも同様)。

  • インラインブロックボックスのマージンは相殺されない(その通常のフロー内の子との間でも同様)。

  • 通常のフロー内のブロックレベル要素の下マージンは、その次の通常のフロー内のブロックレベル兄弟の上マージンと常に相殺される。ただし、その兄弟がクリアランスを持っている場合を除く。

  • 通常のフロー内のブロックレベル要素の上マージンは、その最初の通常のフロー内のブロックレベルの子の上マージンと相殺される。条件は、その要素に上ボーダーと上パディングがなく、かつその子がクリアランスを持っていないことである。

  • 'height' が 'auto' かつ 'min-height' が0の通常のフロー内のブロックレベルボックスの下マージンは、その最後の通常のフロー内のブロックレベルの子の下マージンと相殺される。条件は、そのボックスに下パディングと下ボーダーがなく、かつその子の下マージンがクリアランスを伴う上マージンと相殺されていないことである。

  • ボックス自身のマージンも相殺される。条件は、 'min-height' プロパティが0で、上下のボーダーも上下のパディングもなく、 'height' が0または 'auto' で、かつ行ボックスを含まない場合である。このとき、もし存在すれば、そのすべての通常のフロー内の子のマージンも相殺される。

簡潔にまとめると、わずか4つのルールになります:

  • 通常のフロー外(絶対配置またはフロート)は相殺されない。

  • 新しいBFCの作成をトリガーするもの(フロート、絶対配置要素、ブロックボックス以外のブロックコンテナ、および 'overflow' が 'visible' ではない特定のブロックボックス)は、子と相殺されない。

  • ブロックレベル以外のボックス(インラインブロック)は相殺されない。

  • 一般的な状況下では、兄弟要素の上下マージン、親子要素の上マージン・下マージン、要素自身の上下マージンは相殺される。

最初の3つのポイントは「隣接」の前提条件(インフロー、同一BFC、ブロックレベルボックス)に対するもので、4番目のポイントは4つの「隣接」シナリオを言い換えたものであり、それを詳しく説明したものが8つの推論です。

4. 相殺の挙動

2つの隣接するマージンが相殺された後に形成されるマージンを、相殺されたマージン(collapsed margin)と呼びます。

P.S. collapsed margin は、相殺という動作と区別するために、結果として「相殺されたマージン」と訳しています。

マージンの相殺には2つの特徴があります:

  • 再帰的:すなわち、深い階層での相殺。一度相殺された後、その結果と隣接する別のマージンがあるかチェックし、あればさらに相殺を繰り返します。

  • 貪欲(最大値追求):最も広い相殺結果を求めます。2つのマージンが正の値の場合は最大値をとり、2つが負の値の場合は絶対値の最大値をとります。

再帰的な特性について、「隣接」の定義は次のような再帰的な公式に拡張されます:

相殺されたマージンも、そのマージンの任意の一部が別のマージンと隣接していれば、そのマージンと隣接しているとみなされます。

貪欲さは相殺結果の計算方法に関わります。マージンには負の値が許容されるため、状況は少し複雑になります:

  • どちらも正の値の場合、単純に両者の最大値をとります。

  • 一方が正で他方が負の場合、両者を足し合わせます。

  • どちらも負の値の場合、両者の絶対値の最大値(より小さい方の値)をとります。

例えば:

ul {margin-bottom: -15px;}
  /* インデントはドキュメント構造の入れ子関係を表します */
  li {margin-bottom: 20px;}
h1 {margin-top: -18px;}

このとき、 h1 と最後の li の垂直距離は 20 + -max(|-15|, |-18|) = 2px となります。

正の値であれ負の値であれ、最大値を求める原則は相殺結果をできるだけ広くすること(絶対値が大きい負の値ほど、要素の内容を遠くへオフセットさせるため)であり、これが貪欲性です。

5. オンラインデモ

デモのアドレス:http://ayqy.net/temp/margin-collapse.html

P.S. 答えはすべてデモの中にあり、解説はすべてソースコードの中にあります。

参考資料

コメント

コメントはまだありません

コメントを書く