CSS: marginの正しい理解

toggle()や変数、calc、:matchなど、今までにないCSSプロパティ、セレクタが提案・実装されて、CSS3, CSS4も楽しくなってきています。
border-radiusや、box-shadowなども、古いAndroidブラウザ以外なら、prefixなしで使える状況も増えてきました。

最新技術は、これから必要になってくるかもしれませんが、基本も大切です。
float や position など、CSSコーディングを悩ませるタネはいくつもありますが、今回はその中でも私がCSSで一番難しいと思う margin について書きます。

「marginはバグが多い」という声をたまに聞きます。
しかし話を聞いてみると、正常な動作をバグと間違って認識しているケースもあります。
marginを正しく理解することによって、効率的なレイアウトを構築できますので、基本的な内容ですが、読んで頂ければ幸いです。

ボックスモデル

marginを説明する上で、まずはボックスモデルを知る必要があります。
CSSを適用する場合、以下の図を理解する必要があります。

CSSでmarginを指定すると、上記のボックスモデルのmarginと書かれた部分に、数値が適用されます。

marginの種類

marginにはプロパティが用意されており、省略も可能です。

個別指定

  • margin-top
  • margin-bottom
  • margin-left
  • margin-right

marginの省略

  • margin:0; (上下左右)
  • margin: 0 0; (上下) (左右)
  • margin:0 0 0; (上) (左右) (下)
  • margin:0 0 0 0; (上) (右) (下) (左)

論理プロパティ

  • margin-start
  • margin-end
  • margin-before
  • margin-after

margin-topなどは見慣れてるけど、margin-startは初めて見るという方もいると思います。
margin-(start|end|before|after) は回転方式のブロックに対して、提案されたものです。

margin-topは、画面から見た物理的な方向に対し、
margin-startは、論理的な方向になります。

横書きにmargin-startが指定されている場合は、margin-leftが適用されます。
縦書きになった場合は、margin-startは、margin-topが適用されることになります。

このプロパティを使用することにより、同一のCSSで縦書き、横書きの切り替えがスムーズになる可能性を持っています。(ただしIE8などは使用不可。)

margin と auto と width の関係

marginに対して、autoを設定した場合は数値は0になります。
しかし、横幅(width)を指定した状態で、marginの左右のどちらかにautoを指定すると、指定した方に数値を算出します。

ブラウザの幅が600pxの場合

div {
  with:100px;
  margin-left:auto;
}

上記の例なら、ブラウザの幅が600pxなので、margin-leftは自動で500pxになります。

marginの中央配置

横幅を指定した状態で、margin-left, margin-right にautoを指定した場合。
左右等分のmarginが算出され、ボックスは中央に配置されます。

Negative Margin(ネガティブ・マージン)


marginの数値をマイナス指定することによって、要素を移動や、要素を配置します。
パターンから外れたレイアウトを組む際に、お世話になることが多いです。

例えば、以下の図のようなレイアウトにしたい場合

<style>
/* css reset */
ul {
  margin:0;
  padding:0;
  list-style:none;
}

/* demo */
#list {
  margin:0 auto;
  border:1px solid #000;
  padding:8px 8px 0;
  width:488px;
}
ul {
  margin-right:-8px; /* ここでネガティブマージンを指定 */
  /* width:496px; 追記:IE6,7を無視するならいらない */
  overflow:hidden;
}
li {
  float:left;
  margin:0 8px 8px 0;
  width:116px;
  height:116px;
  background-color:#bbb;
}
</style>

<div id="list">
  <ul>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
    <li></li>
  </ul>
</div>

上記ソースのデモ

ネガティブマージンを使用すると、レイアウトの幅が広がります。
ただし上記ソースの1つ1つの意味が、理解できていないうちは、多用すべきではありません。

margin と padding

marginでも、paddingでも、空間をあけることが可能です。

<style type="text/css">
.margin {
  margin-bottom:10px;
}
.padding {
  padding-bottom:10px;
}
</style>

<div class="margin">aa</div>
<div class="margin">bb</div>

<div class="padding">aa</div>
<div class="padding">bb</div>

margin, padding共に、空間をあけることができます。
見た目は同じに見えますが、挙動は全く違います。

paddingは、背景色や、枠線にも影響して余白をあけることができますが、
marginは要素ごとで間隔をあけることができます。

marginの相殺

paddingにはない機能として、marginの相殺があります。
相殺はmargin同士が重なった場合に起こり、「marginが使いづらい」という印象を与える最大の理由かもしれません。
特定のケースだと、相殺が起きないなどトリッキーな動きをします。
トリッキーすぎて、初学者に「バグじゃないの?」と思われることも、しばしばです。

しかしmarginの相殺を理解することで、marginの単一方向の指定や、paddingを使用するよりも、HTML, CSSをクリーンに書ける場合も多くあります。

<style type="text/css">
.margin {
  margin:10px 0; /* 上下に1emを指定 */
}
.padding {
  padding:10px 0; /* 上下に1emを指定 */
}
</style>

<div class="margin">aa</div>
<div class="margin">bb</div>

<div class="padding">aa</div>
<div class="padding">bb</div>

paddingであれば、divの間は、10px+10px=20px となります。
marginは相殺によってdivの間が、10pxしかあきません。

相殺を理解するには、要素を人に例えるとわかりやすいかもしれません。

  • widthは、骨格
  • heightは、身長
  • borderは、皮膚
  • paddingは、脂肪
  • marginは、心の距離

脂肪(padding)だと、物理的な距離となります。

これが心の距離(margin)の場合は、Aさんの心の距離が1。
ただしBさんのAさんのことを、それほど想っておらず、心の距離は3だとします。
Aさんは心が縮まっていると思っていても、AさんとBさんの心の距離を客観的に見ると、実際の心の距離は3となります。

marginの相殺の数値

相殺は、数値の大きい方が適用されます。

<style type="text/css">
  .a {
    margin:20px 0;
  }
  .b {
    margin:30px 0;
  }
</style>

<div class="a">a</div>
<div class="b">b</div>

上記の例では、div同士の間隔は、20pxと30pxで50pxになるはずですが、相殺によって、少ない方の数値が打ち消されます。

入れ子での相殺

相殺は入れ子の状態でも、marginが重なれば発生します。

両方ともにmarginが設定された場合、相殺が発生します。

<style type="text/css">
  .parent{
    margin:10px 0;
  }
  .child {
    margin:30px 0;
  }
</style>

<div class="parent">
  <div class="child">child</div>
</div>

入れ子の別の例

<style type="text/css">
  .parent {
    margin:30px 0;
  }
  .child {
    margin:10px 0 40px;
  }
</style>

<div class="parent">
  <div class="child">demo</div>
</div>

上記の例では、margin-topは、.parentの方が大きいので30pxが適用されます。
しかしmargin-bottomは、.childの方が値が大きいので40pxが適用されることになります。
相殺が起こった場合は、大きい方の値が優先されることを覚えておいて下さい。

相殺が起こらないケース

親の要素に特定のCSSを適用することで、子の相殺が発生しません。

  • border (marginが指定されている方向)
  • padding (marginが指定されている方向)
  • overflow:hidden, scroll (auto, visibleは適用されない)
  • position:absolute, fixed (relative, staticは適用されない)

さらに上記以外にも、floatによって相殺が発生しない場合があります。

margin と float の関係

marginとfloatは、密接な関係にあります。
floatを使いこなすためにも、marginの理解は必須です。

marginのかかっている要素に、floatを指定した場合

親、子に関わらずmarginを指定している要素に、floatを適用すると相殺をしなくなります。

また width と margin:auto; の関係で記載したように、widthとmarginを指定した場合のautoは、数値を自動算出します。
ただしfloatが指定されると、marginのautoは、必ず数値が0になります。

margin: auto;とfloatを指定した例

<style>
div {
  margin:10px auto;
  width:300px;
  float:left;
}

.child {
  margin:10px;
  width:100px;
  height:100px;
  float:left;
}

</style>

<div>
  <div class="child"></div>
  <div class="child"></div>
  <div class="child"></div>
  <div class="child"></div>
</div>

  • 親か子のどちらかでもfloatを指定すると、親子での相殺が起こらない。
  • floatが指定されると、margin:auto; は絶対的に0になる。
  • 兄弟同士にfloatが指定されていると、相殺が起こらない。

floatを使用すると、その要素のmarginはpaddingのような挙動を行います。

marginのかかっている要素に、floatが隣接している場合

floatが親、子の要素にかかると相殺が発生しません。
ただfloatが隣接しているだけなら、marginとfloatで相殺が発生します。
以下はその基本的な条件です。

  • floatに隣接
  • 隣接している要素に、marginが指定されている
  • 隣接している要素に、floatが指定されていない

上の条件が全てではありません。
様々な影響により、相殺が発生する場合としない場合があるので割愛します。

以下、marginとfloatの相殺の例です。

<style>
img {
  float:left;
  margin:0 10px;
}
.test {
  margin-top:96px;
}
</style>

<img src="dummy.jpg" alt="" width="80" height="80" />
<img src="dummy.jpg" alt="" width="80" height="80" />
<img src="dummy.jpg" alt="" width="80" height="80" />
<div class="test">floatとmarginの相殺</div>

1つの要素にmarginとfloatが指定されていると、marginの相殺が起きません。

しかし上の例のdiv.testは、marginとfloatが相殺する条件を満たしているので、図のような動きをします。
そしてこの相殺は、floatを解除するclearプロパティにも関係してきます。

clearは、floatを解除するものではない

「floatを解除する」とよく説明されるclearプロパティですが、floatに隣接してはならないかを指示するプロパティです。
その挙動は、先ほどのサンプルによく似ています。

'clear'プロパティの仕様

'none'以外の値は、潜在的にクリアランスを導入する。クリアランスはマージンの相殺を抑制し、要素のmargin-topの上へ空白のように振る舞う。浮動体を過ぎて垂直に要素を押すために使用される。

'clear'が設定された要素のクリアランスの計算は最初に要素の上ボーダー辺の仮の決定によってされる。要素の'clear'プロパティが'none'である場合、この位置は実際の上ボーダー辺となる。

9.5.2 浮動体に隣接するフローの制御: 'clear'プロパティ - W3C 視覚整形モデル (邦訳)

clearは、marginの相殺を抑制し、floatの底辺に要素がつくように、margin-topの値を自動調整するような振る舞いを行います。
clear:none;の場合は、通常通りに振舞います。

以下、margin, float, clearの例です。

<style>
img {
  float:left;
}
.right {
  float:right;
  width:80px;
  height:240px;
  margin-bottom:10px;
}
.clear {
  margin-top:3000px;
  clear:left;
}
.clear-right {
  margin-top:-1000px;
  clear:right;
}
</style>

<img src="dummy.jpg" alt="" width="80" height="80" />
<img src="dummy.jpg" alt="" width="80" height="80" />
<img src="dummy.jpg" alt="" width="80" height="80" />

<div class="right"></div>

<div class="clear">clear:left;</div>
<div class="clear-right">clear:right;</div>

.clearにmargin-topに3000pxも指定しています。 本来なら、3000pxの間隔が空くはずですが、float:leftを指定したimgの底辺にくっついてしまっています。
これはclearによる仕様です。
clear:left;を指定したことによって、margin-topが自動調整されて、float:left;の指定された要素の後ろにつきます。

さらに、float:right;を指定した.rightの底辺(marginを含む)には、.clear-rightの上辺が重なっています。
この場合、.rightの上辺から底辺までのmargin-topを調整している訳ではありません。
.clearから.rightの底辺までの距離で、margin-topを調整しています。

なぜなら、隣接するfloatを指定した要素とmarginは、相殺関係になりますが、.clearのように隣接する要素とmarginに相殺関係が発生しない場合は、そこからが出発点になります。

clearとmargin-topを同時に適用すると、clearプロパティが優先されるので気をつけて下さい。

相殺の利点

うんざりしてくるほど、ややこしいmarginですが、間隔を適切にあけたい場合。
相殺など利用することにより、クリーンなソースコードが書ける場合も多くあります。
レイアウトをmarginで書いたものと、paddingで書いたものを比較します。

完成するレイアウト

margin

<style type="text/css">
dl {
  margin:3em 0;
}
dt {
  display:inline;
  float:left;
}
dd {
  margin:1em 0 3em 120px;
}
p {
  margin:1em 0;
}
</style>

<dl>
  <dt>margin デモ1</dt>
  <dd>ddタグの中に直接テキストを入れています。</dd>

  <dt>margin デモ2</dt>
  <dd>
    <p>ddタグの中にpタグを入れています。</p>
    <p>ddとpのmarginが重なるので、相殺が起きます。</p>
  </dd>

  <dt>margin デモ3</dt>
  <dd>
    <img src="dummy.jpg" alt="" width="480" height="32">
    <p>画像が入っても適度に間隔があきます。</p>
    <p>また下に入っても問題ありません。</p>
	<img src="dummy.jpg" alt="" width="480" height="32">
	<p>さら下に入っても問題ありません。</p>
  </dd>
</dl>

padding

<style type="text/css">
 /* CSSリセット */
dl, dd, p {
  margin:0;
}

dl {
  padding:3em 0;
}
dt {
  display:inline;
  float:left;
}
dd {
  padding:0 0 3em 120px;
}
.pt {
  padding-top:1em;
}
.pb {
  padding-bottom:1em;
}
.last-child {
  padding-bottom:0;
}
</style>

<dl>
  <dt>margin デモ1</dt>
  <dd>ddタグの中に直接テキストを入れています。</dd>

  <dt>margin デモ2</dt>
  <dd>
    <p class="pb">ddタグの中にpタグを入れています。</p>
    <p>pタグ同士の間隔をあけるので、classを入れなければいけません。</p>
  </dd>

  <dt>margin デモ3</dt>
  <dd class="last-child">
    <img src="dummy.jpg" alt="" width="480" height="32">
    <p class="pt pb">画像が入ると、class祭りになります。</p>
    <p class="pb">新しくpタグを入れようと思うと、classを更に入れなければなりません。</p>
	<img src="dummy.jpg" alt="" width="480" height="232">
	<p class="pt">classがやたら増える。</p>
  </dd>
</dl>

marginの相殺を利用したものと、paddingを使ってレイアウトを組んだものの比較です。
paddingを使用すると、最後の要素は、padding-bottom:0; を指定したり、classを細かく割り振ったりします。
.mt10, .mt20などのクラスを用意している人は、paddingのような指定を行っていることが多いです。

それに対して、marginの相殺を利用すると、HTML, CSSともに、短く書くことができます。
さらに適切に間隔を保つことができるので、特にCSSを指定していないimg要素がイレギュラーに入ったとしても、ある程度は容易に対処することができます。

上記のサンプルは、相殺をうまく利用した例です。
デザインによっては、親にborderやpaddingなどを入ることにより、相殺が使えない場合もあります。
しかし、相殺を理解していることで、HTMLやCSSの書く効率が変わってきます。

ブラウザのデフォルトスタイルシート

ブラウザのデフォルトスタイルシートは、marginと関連があります。
h1やpなどの要素には、ブラウザで、あらかじめCSSがついています。

h1 {
  margin:0.67em 0;
  font-size:2em
}
p {
  margin:1em 0;
}

相殺を計算されたうえで、上下にmarginの指定されています。
このスタイルシートによって、"文章"を適切にマークアップすれば、適度な間隔を保つようになっています。

デフォルトスタイルシートを生かすように構築する場合は、相殺の考え方が大切になります。

marginの仕様とバグは、度々一緒にされることがあります。
marginは、奥深く、経験もある程度必要になります。
marginのバグも回避策は多くあるので、マスターすればCSSの構築に役立ってくれるプロパティになるはずです。