HTML要素の位置設定

趣旨

昨日のエントリで HTML 要素の位置取得について述べた。取得とくれば今度は設定であろう。位置設定にも、いくつかの落とし穴があるので、解説したい。

情報ソース

位置取得同様 W3C CSS 2.1 の次の各章が重要である。

これはまだ草稿(draft)レベルなので、仕様としては参照しにくいが、CSS 3 のボックスモデルの解説は CSS 2.1 を整理してかなり読みやすい文章になっているのでおすすめ。

ウェブページ上のピクセル数を正確に測るには以下のブックマークレットが便利だ。

注意

以下では、position:absolute な HTML 要素の位置設定に話を限定する。じつは position:relative な要素にも位置を設定できるのだが、そちらの話のほうはもう少し簡単なので、他を当たっていただくということで。

記法

以下では「A または B」を A|B のように表記する。

基本概念

要素と生成されたボックス

昨日のボックスモデルについて敷衍したい。CSS 仕様によれば、要素(element)は 0 個以上のボックス(box)を生成する(CSS21:9.1 CSS3BOX:2)。 ボックスとは、コンテンツエッジ・パディングエッジ・ボーダーエッジ・マージンエッジからなる4重の長方形であり、文字列や画像等、コンテンツが描画される位置や大きさの基準となるものである。要素とは、HTML 文書をパースして得られたノードツリーの1ノードのことだ。であるから、要素はあくまでも HTML 文書の構成要素であり、視覚的なものではないことに注意したい。要素が生成するボックスこそが、ブラウザのスクリーン上で視覚効果を作り出すのだ。

仕様では、要素は 0 個以上のボックスを生成するというやや含みのある言い方をしている。display:none な要素は、文書中には存在しているが、一切ボックスを生成しない(CSS21:9.2.4)。 また1つの要素が2つ以上のボックスを生成することもある。たとえば、CSS3BOX:2はリストの中の LI 要素が2つのボックス(リストの見出しアイコンを含むボックスと文字列を格納する行ボックス)を生成する例をあげている。

包含ブロック(containing block)

CSS21:10.1は包含ブロック(containing block)について述べている。包含ブロックとは、ある要素の生成するボックスの位置や大きさを決める基準となる長方形のことである。ある要素の包含ブロックは、通常は、その要素のいずれかの祖先要素が生成するボックスの4つの長方形のうちの一つである。はっきり理解したいのは、包含ブロックはボックスではない、ということだ。CSS ではボックスは必ず4つの長方形=エッジをもつ。しかし、包含ブロックは単純な長方形にすぎない。

包含ブロックは、postion:absolute な要素だけでなく、すべての要素に存在する。position:static な要素の位置を指定することはできないのに、なぜ包含ブロックを考える必要があるのだろうか?それは、その要素の幅や高さをパーセントで指定したい場合に意味を持ってくる。そのパーセント指定の基準になるのが包含ブロックなのである。

包含ブロックのなかで特別なもので、初期包含ブロック(initial containing block)というものがある。*1 これは、ルート要素(HTML 要素)が所属する包含ブロックであり、幅と高さはビューポート(表示域)と同じで、キャンバス座標の原点に据え付けられている。要するに画面を左上にスクロールしきったときののビューポートそのものと考えればいい。

さてここまで話したところで、お題目の position:absolute な HTML 要素の位置設定の話である。位置指定をするには、まず基準となる包含ブロックを求めなければならない。position:absolute な要素の包含ブロックは次のルールで定められる。*2

  1. 先祖要素のなかで自分に一番近い position:relative|absolute|fixed な要素が生成するボックスのパディングエッジ
  2. でなければ、初期包含ブロック

例のボックス長方形4兄弟のうち、パディングエッジが包含ブロックとして使われるのだ。このことはよーく覚えている必要がある。*3

後は、要素の top/ left プロパティに設定を行えば、position:absolute な要素の位置は指定できる。たとえば top の場合は「包含ブロックの上辺から要素のマージンエッジ上辺への距離」であり、left は「包含ブロックの左辺から要素のマージンエッジ左辺への距離」である。マージンエッジである!ボーダーエッジではない。注意しよう。まとめると下図のようになる。

element1 は position:absolute で、element2 が element1 にとって position:relative|absolute|fixed な直近の先祖要素だとしよう。するとプロパティ top で指定できる距離は、element1 のパディングエッジ上辺から element2 のマージンエッジ上辺までの距離になる。同様にプロパティ left で指定できる距離は element1 のパディングエッジ左辺から element2 のマージンエッジ左辺までの距離になる。

現実

幸い上の仕様は現在のブラウザによって、きちんと実装されているようだ。次のような HTML 文書を考えてみる。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>positioning</title>
<style type="text/css">
body {
	position: static;
	margin: 10px;
	border: 5px solid red;
	padding: 20px;
}

#div1 {
	position: absolute;
	margin: 10px;
	border: 5px solid blue;
	padding: 15px;
	left: 0px;
	top: 0px;
	width: 200px;
	height: 300px;
}

#div2 {
	position:static;
	margin:12px;
	border:5px solid purple;
	padding:18px;
}

#div3 {
	position: absolute;
	margin: 20px;
	border: 5px solid yellow;
	padding: 15px;
	left: 0px;
	top: 0px;
	width: 200px;
	height: 300px;
}

</style>
</head>
<body>
<div id="div1">
<div id="div2">
<div id="div3">
</div>
</div>
</div>
</body>
</html>

これを Firefox 2.0.0.11 で表示させたものが下図である。body, div1, div2, div3 の各要素がそれぞれ赤、青、紫、黄のボーダーで表示されている。

HTML要素 > BODY要素 > div1 > div2 > div3 という親子関係がある。このうち position:absolute なのは div1(青) と div3(黄) のみである。

  • div1 の祖先要素(BODY要素, HTML要素) には position:relative|absolute|fixed なものがない。そのため包含ブロックは初期包含ブロック(=ビューポート)になる。div1 のマージンエッジの左上の点がちょうどキャンバス座標原点にきていることがわかるだろう。(といっても、マージンエッジは透明なのでわかりずらい。上で紹介したピクセル数測定ブックマークを活用してほしい)
  • div3 の祖先要素には div1 という position:absolute な要素がある。したがって、div3 の包含ブロックは div1 の生成するボックスのパディングエッジになる。div3 の left, top は 0px なのだが、ちょうど div1 と div3 のボーダーエッジ間は div3 のマージン分(20px) 離れていることがわかるだろう。また div3 の直接の親要素である div2(紫) が div3 の位置決めに何の関与もしていないことを確認してほしい。

ここで、BODY要素 の position:relative に変えてみると様相がかわる(下図)

div1 は body の右下 10px ずつのところに位置するようになる。これは BODY要素が position:relative になったことで、div1 の包含ブロックを形成するようになったからだ。このことからも BODY要素というのは、ルート要素ではなく、位置決めという観点からは、ごく普通の要素の一つにすぎないということがわかる。

私が実験したかぎり唯一変則的だったのは Quirk(互換)モードにおける IE 6 であった。body を position:static と指定したのにもかかわらず、下図のように表示された。

どうやら、BODY 要素 のパディングエッジが初期包含ブロックという解釈らしい。div1 にキャンバス座標そのものを設定するつもりで、top, left を指定しても、この場合、ボーダー幅の分だけ右下にずれて、表示されることに注意したい。(たとえば top:100px と指定しても、BODY 要素の border-top-width が 5px とすると、キャンバス座標の 105px の位置に表示される結果になる。left についても同様) ただ興味深いのは、offsetTop を計算するとこれまた BODY 要素のボーダー分ずれているようである。(キャンバス座標の 105px に位置する要素について、 BODY 要素の border-top-width が 5px なら、offsetTop は 100 となる) ということで、要素間の相対関係のみが関係するウェブアプリについては影響ないかもしれない。

包含ブロックと offsetParent の微妙な関係

これまでの説明を聞いて、「包含ブロックを形成する要素って、offsetParent じゃないの?」という疑問がわくかもしれない。position:absolute な要素については、だいたいはそうかもしれない。*4 それ以外の要素については、包含ブロックと offsetParent は関係ないようだ。たとえば、次のような HTML では、position:static な span1 の包含ブロックを形成する要素は CSS 仕様によれば、直近の display:block な祖先要素である div1 でなければならない。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<body>
<div id="div1" style="position:static">
<span id="span1" style="position:static">#span1</span>
</div>
</body>
</html>

しかし span1 の offsetParent を計算すると、BODY 要素が得られる。これは、span1 の包含ブロックを形成する要素ではない。したがって包含ブロックと offsetParent は別物と考えておいたほうが安全であるようだ。

*1:"initial" を「初期」と訳すのが通例らしいが、なんかしっくりこない。「原始包含ブロック」と呼ぶほうが自分的にはしっくり行くのだが、ここでは慣例にしたがって「初期包含ブロック」としておく

*2:CSS仕様では、ルール1 において、その当該祖先要素が display:inline か display:block で場合わけしているが、本質的な違いではないので割愛する

*3:position:static な要素では、包含ブロックは、祖先要素のなかで自分に一番近いブロックレベル要素の生成するボックスのコンテンツエッジになる。全然ルールがちがうので混乱しないように

*4:offsetTop/offsetLeft/offsetParentの闇によれば、IE6 において、position:absolute な要素の祖先要素のうち、position:static だが width, height を指定しているブロックレベル要素が offsetParent になる例が目撃されている