CSSとJavaScriptによって、Webページの中央(水平・垂直方向)に、高さと幅がわからない1つの画像を、ブラウザの表示領域の外にはみ出すことなく配置する、6つの方法を考えた(Chrome、Firefoxのみ) ~本当の「中央」はどれか?~

この記事の修正版→http://syoichi.tumblr.com/post/2650814760

最近Tumblrのテーマのカスタマイズを通してHTML、CSS、JavaScriptの扱い方を少しずつ学んでいる。その中で、Tumblrのポストページにおいて画像のみをブラウザの画面の中央に表示したいと考え、その方策について試行錯誤を重ねてきた。

何をしたいのか: Chrome、Firefoxだけでいいので、Webページの中央(水平・垂直方向)に、高さと幅がわからない1つの画像を、ブラウザの表示領域の外にはみ出すことなく配置したい

あくまで個人用のテーマを作っているので、他のユーザーが閲覧することをあまり想定せず、常用しているブラウザ以外のInternet ExplorerやSafari、Operaなどでの動作は考えていない。また、無闇にクロスブラウザを志向して、多くの例外処理を施していくコストはあまりに高くつく。よって、Chrome、Firefoxのみで表示の確認をし、実現できるかどうか調べていくことにした。

画像をWebページの水平方向と垂直方向で中央に表示する事は、最初はCSSのみでできると思っていた。ところが、水平方向での中央表示は、「text-align: center;」や「margin-left: auto;」と「margin-right: auto;」の組み合わせ(あるいは「margin: 0 auto;」)で割と簡単にできても、垂直方向での中央表示がなかなかできなかった。どの方法も画像やブラウザ表示領域のサイズがわかっていないと駄目だったからだ。Tumblrでは様々なサイズの画像が存在するので、事前にサイズを知る事も難しく、予め指定する方法は採用できない。

また、画像をブラウザ表示領域内に収まるように表示する事も同様だった。ちなみに、「画像をブラウザ表示領域内に収まるように表示する」を厳密に言うと、画像のサイズがブラウザ表示領域を超える場合は、領域内に収まるようにリサイズするが、超えない場合は画像本来のサイズで表示するという事だ。つまり、小さい画像を無理やり大きく表示しないという方針になる。これを実現しようとしたところ、Chromeでは「max-height: 100%;」と「max-width: 100%;」の組み合わせでできても、Firefoxではブラウザ表示領域のサイズを直接指定しないと同じ表示にはならない場合があり、このCSSの組み合わせだけではできないことがわかった。

そこでJavaScriptによって、画像やブラウザ表示領域のサイズを取得し、その値とそれを必要とするCSSプロパティと共にstyle要素に追加していくことにした。

このCSSとJavaScriptの組み合わせで色々と調べていくうちに、やりたいことを実現するのに6つぐらいの方法があることがわかったが、調べた限りではこうした方法をまとめたページがなかったので、今回ソースとサンプルページと共にまとめてみた。ただし、まだまだ勉強不足なので、誤って認識している可能性が非常に高く、あくまで個人的なメモという位置付けであることに注意してもらいたい。もちろんこの他により良い方法があるならば是非教えて頂きたいし、間違っているところがあれば適宜指摘してほしい。

HTMLは簡略化のため、HTML5の表記を用いている。また、CSSの追加にはinsertRuleの使用で統一している。これは「これでできる! クロスブラウザJavaScript入門:第20回 JavaScriptによるスタイルの操作|gihyo.jp … 技術評論社」を参考にした。

前述の通り、クロスブラウザ、クロスプラットフォームは考慮していない。Windows XPにおけるChrome 8.0.552.224、Firefox 3.6.13上のみで最終確認をしている。

1. ブラウザ表示領域の高さから、画像がブラウザ上で表示される時の高さ(つまり画像本来のものではない)をマイナスし、それの半分を余白として画像の上に置く

<!doctype html>
<meta charset=utf-8>
<title>Case 1</title>
<style>
body {
margin: 0;
}
img {
display: block;
margin-left: auto;
margin-right: auto;
}
</style>
<img src=http://gyazo.com/8e52ce00a418a0ae0bdeb16e87f0a1de.png>
<script>
var h = window.innerHeight, s = document.styleSheets[0];
s.insertRule('img{max-height:' + h + 'px;max-width:' + window.innerWidth + 'px;}', s.cssRules.length);
window. {
s.insertRule('img{margin-top:' + ((h - document.images[0].height) / 2) + 'px;}', s.cssRules.length);
};
</script>

http://jsbin.com/okuqo4

CSSに関しては、body要素に「margin: 0;」の指定をしているのはUser Agentに設定されているデフォルトスタイルシートを無効にするため。また、わざわざimg要素に「display: block;」を指定しているのは、何故かinline要素の状態だと7pxほど下に余白が付いてしまうので、block要素にしてその余白を消すため。

image
image

黒の画像と白の背景の境にある薄水色の部分が余白。「vertical-align: middle;」などの指定でも消えるようだ。

しかし、img要素の親要素が、href属性が設定されたa要素だと、画面全体がリンクとしてクリックできるようになってしまうという問題があるようだ。

JavaScriptに関しては、document.documentElement.clientHeightとdocument.documentElement.clientWidthではなく、window.innerHeightとwindow.innerWidthを使用しているのは、画像をブラウザ表示領域内に必ず収める事を目的としているため、ページが画面外にはみ出す場合を想定する必要がなく、スクロールバーのないブラウザ表示領域のサイズの取得で問題ないからだ。また、画像がブラウザ上で表示される時の高さが必要であるため、画像本来の高さを表すnaturalHeightではなくheightを使用している。

画像の表示の処理が遅いと、画像が一旦画面の一番上に表示されてから中央に表示されるように見える事がある。

2. 画像を本来表示される位置から、ブラウザ表示領域の高さと幅のそれぞれ半分ずつ右方向と下方向に移動させ、画像がブラウザ上で表示されるときの高さと幅のそれぞれ半分ずつ左と上の余白を詰める

<!doctype html>
<meta charset=utf-8>
<title>Case 2</title>
<style>
body {
  margin: 0;
}
img {
  position: relative;
  top: 50%;
  left: 50%;
}
</style>
<img src=http://gyazo.com/8e52ce00a418a0ae0bdeb16e87f0a1de.png>
<script>
var h = window.innerHeight, w = window.innerWidth, s = document.styleSheets[0];
s.insertRule('body{height:' + h + 'px;width:' + w + 'px;}', s.cssRules.length);
s.insertRule('img{max-height:' + h + 'px;max-width:' + w + 'px;}', s.cssRules.length);
window. {
  s.insertRule('img{margin-top:-' + (document.images[0].height / 2) + 'px;margin-left:-' + (document.images[0].width / 2) + 'px;}', s.cssRules.length);
};
</script>

http://jsbin.com/okuqo4/2

CSSで上下左右中央に画像を配置する」を元にしている。「CSSで画面全体に対して天地中央に配置するテクニック(1) | 先端研究ブレイドLAB」も参考になる。

画像の表示の処理が遅いと、画像が一旦画面の右下に表示されてから中央に表示されるように見える事がある。

3. body要素を本来表示される位置から、ブラウザ表示領域の高さと幅のそれぞれ半分ずつ右方向と下方向に移動させ、画像をブラウザ上で表示される時の高さと幅のそれぞれ半分ずつを、body要素の位置を基準にして左方向と上方向に移動させる

<!doctype html>
<meta charset=utf-8>
<title>Case 3</title>
<style>
body {
position: fixed;
top: 50%;
left: 50%;
margin: 0;
}
img {
position: relative;
}
</style>
<img src=http://gyazo.com/8e52ce00a418a0ae0bdeb16e87f0a1de.png>
<script>
var s = document.styleSheets[0];
s.insertRule('img{max-height:' + window.innerHeight + 'px;max-width:' + window.innerWidth + 'px;}', s.cssRules.length);
window. {
s.insertRule('img{top:-' + (document.images[0].height / 2) + 'px;left:-' + (document.images[0].width / 2) + 'px;}', s.cssRules.length);
};
</script>

http://jsbin.com/okuqo4/3

これでできる! クロスブラウザJavaScript入門:第20回 JavaScriptによるスタイルの操作|gihyo.jp … 技術評論社」を元にしている。

6つの方法の中で唯一、ブラウザの画面サイズを変えても画像が中央に表示されるようになっている。

また、2と同じく画像の表示の処理が遅いと、画像が一旦画面の右下に表示されてから中央に表示されるように見える事がある。

4. 画像を背景に設定し、「background-position: center;」によって水平方向と垂直方向で中央に表示し、画像のサイズがブラウザ表示領域を超える時は「background-size: contain;」を指定して領域内に収める

<!doctype html>
<meta charset=utf-8>
<title>Case 4</title>
<style>
body {
margin: 0;
background-position: center;
background-repeat: no-repeat;
}
</style>
<script>
var h = window.innerHeight, i = new Image(), s = document.styleSheets[0];
i.src = 'http://gyazo.com/8e52ce00a418a0ae0bdeb16e87f0a1de.png';
i. {
s.insertRule('body{background-image:url(' + i.src + ');' + ((h < i.height || window.innerWidth < i.width) ? '-moz-background-size:contain;background-size:contain;' : '') + 'height:' + h + 'px;}', s.cssRules.length);
};
</script>

http://jsbin.com/okuqo4/4

img要素を用いない方法。1~3では画像をブラウザ表示領域内に収めるのにmax-height、max-widthを指定していたが、背景画像の場合は「background-size: contain;」を指定する。ただし、画像がブラウザ表示領域内に収まるサイズのときは、ブラウザ表示領域いっぱいまで拡大して表示されてしまうので条件分岐が必要だった。ちなみに、背景画像を指定している時はbody要素に幅の指定をする必要はないようだった。

ところで、1~4の場合、「画像がブラウザ上で表示される時のサイズ」を取得するには画像の読み込みが完了していて、且つ画像がブラウザ上で描画されている事が条件になる。後者は、例えばimg要素に「display: none;」や「opacity: 0;」を指定していたら取得できないという事だ。よって、画像の読み込み完了を待つ時間がどうしても必要となり、画像を素早く快適に閲覧していく事ができなくなる。特に画像のファイルサイズが大きかったり、画像の読み込み速度が遅い環境だと、ページの表示に関して、重大なスタイルの崩れや遅延を引き起こしかねないと考えられる。特に4は画像の読み込みが完了してから初めて画面上に現れるようになっているので、注意が必要になる。

5. body要素に「display: table-cell;」と「vertical-align: middle;」を指定して垂直方向での中央表示を行う

<!doctype html>
<meta charset=utf-8>
<title>Case 5</title>
<style>
body {
display: table-cell;
vertical-align: middle;
}
img {
display: block;
margin-left: auto;
margin-right: auto;
}
</style>
<img src=http://gyazo.com/8e52ce00a418a0ae0bdeb16e87f0a1de.png>
<script>
var h = window.innerHeight, w = window.innerWidth, s = document.styleSheets[0];
s.insertRule('body{height:' + h + 'px;width:' + w + 'px;}', s.cssRules.length);
s.insertRule('img{max-height:' + h + 'px;max-width:' + w + 'px;}', s.cssRules.length);
</script>

http://jsbin.com/okuqo4/5

異なるサイズの画像を縦横中央配置にしてリスト状に並べる | CSS-EBLOG」を元にした。

table-cell要素では余白の指定は適用されないので、「margin: 0;」の指定は必要ない。

6. body要素に「text-align: center;」で水平方向での中央表示、img要素に「vertical-align: middle;」で垂直方向での中央表示をし、body要素にブラウザ表示領域の高さをline-heightに指定する

<!doctype html>
<meta charset=utf-8>
<title>Case 6</title>
<style>
body {
margin: 0;
text-align: center;
}
img {
vertical-align: middle;
}
</style>
<img src=http://gyazo.com/8e52ce00a418a0ae0bdeb16e87f0a1de.png>
<script>
var h = window.innerHeight, s = document.styleSheets[0];
s.insertRule('body{line-height:' + (h - 2) + 'px;}', s.cssRules.length);
s.insertRule('img{max-height:' + h + 'px;max-width:' + window.innerWidth + 'px;}', s.cssRules.length);
</script>

http://jsbin.com/okuqo4/6

vertical-alignの説明-CSS TIPS」を参考にした。

1~5と比べると、CSSでは一番わかりやすく簡潔な形。ただし、何らかの文字があると、その文字にもブラウザ表示領域の高さが付いてしまう。また、line-heightに指定したブラウザ表示領域の高さから2をマイナスしているのは、何故か画像の表示が領域内に僅かに収まらない場合があるため。とてもスマートな形とは言えないが…。

以前は5に「display: block;」を適用せずに邪魔な余白を受け入れて画像の上側に7pxほどの余白を付け、その分画像の最大サイズを削っていく方法を採用していたのだが、これでは画像を最大限に表示することができなかった。それから色々と調査、比較を行った結果、今のところ6が一番シンプルなので、使い方に注意しつつテーマに採用している。

その他: ブラウザには「中央」を表す位置が複数ある?

上記6つの方法を比較すると、画像の中央表示に僅かな差異がある事が確認できた。画面サイズを変えていくつかの場合を見ると、1、5、6の画像は同じ表示なのだが、2、3、4は数px右側に表示され、4だけ数px下側に表示される。どれが本当の「中央」を表すのだろうか?