スクロール位置を維持しつつ web ページ全体のスクロールをできないようにする方法 (web ページのスクロール位置固定)

最近は Ajax などによって web ページを動的に変更するという手法も一般的になって、ページの中にさらにサブページのようなものをポップアップする、ということがしばしば行われます。 例えば Facebook の Theater mode のようなものです。

さて、「ページの中にサブページのようなものをポップアップして表示する」 場合、サブページがスクロール可能であれば、もともとのページ全体はスクロール不可能にしておく方がユーザーにとっては使いやすいデザインであると思います。 実際、Facebook でも Theater mode の際には Theater 側にスクロールバーがあるのみで、ページ全体はスクロール不可になります。

本記事では、そのような用途に用いるための 「スクロール位置を維持しつつ web ページ全体のスクロール可否性を変更する関数」 を紹介します。

ページ全体をスクロール不可能にする方法

さて、まずはページ全体をスクロールする方法を説明します。 一般的なブラウザにおいては、表示領域として viewport があります。 ブラウザの描画領域のことだと考えてください。 Web ページ全体のコンテンツが描画される領域よりも viewport が小さい場合は、ページ全体をスクロールする機構が viewport に与えられる、というのが一般的です。

つまり、ページ全体のスクロールを不可にするというのは、すなわち viewport のスクロールを不可にする、ということです。 スクロールの可否に関わる CSS のプロパティといえば overflow ですが、CSS 2.1 勧告の overflow の項 に次のように書かれています。

UAs must apply the 'overflow' property set on the root element to the viewport.

HTML 文書であれば、ルート要素である html 要素に overflow: hidden と設定をすると、それが viewport に適用される、ということです。

つまり、ページ全体をスクロール不可能にする方法は、html 要素に overflow: hidden と設定する ということになります。

スクロール位置がリセットされてしまう

しかし、html 要素に overflow: hidden と設定すると、Firefox 5 などではスクロール位置が元のままではなく、ページ上部に戻ってしまいます。 この挙動が仕様で規定されているものなのか実装依存なのかはよくわかりませんが、現実問題としてスクロール位置がリセットされてしまうのでなんとかする必要があります。

スクロール位置の取得と設定

この問題の対応策として、html 要素に overflow: hidden を設定する前のスクロール位置を予め取得しておいて、overflow: hidden の設定後に元のスクロール位置を設定しなおす、という方法があります。

Web ページ全体の (viewport の?) スクロール位置を取得したり設定したりするメソッドは、CSSOM View Module の ScreenView インターフェイス に定義されています。 まだ Working Draft 段階ですが、FirefoxOperaSafari などのブラウザでは既に実装されています。 ScreenView インターフェイスが実装されている実行環境であれば、次のような JavaScript コードで、ページのスクロール位置を取得できます。

// x 方向のスクロール位置
var xos = document.defaultView.pageXOffset;
// y 方向のスクロール位置
var yos = document.defaultView.pageYOffset;

また、IE 6 以降では、次のコードで同様に取得できます。

// x 方向のスクロール位置
var xos = document.documentElement.scrollLeft;
// y 方向のスクロール位置
var yos = document.documentElement.scrollTop;

ページのスクロール位置の変更は、次のようなコードで実行できます。

// ScreenView インターフェイスが実装されている場合
document.defaultView.scrollTo( xoffset, yoffset );
// IE の場合
window.scrollTo( xoffset, yoffset );

スクロール位置を維持しつつ web ページ全体のスクロール可否性を変更する関数

というわけで、まとめ代わりにスクロール位置を維持しつつ web ページ全体のスクロール可否性を変更する JavaScript 関数をおいておきます。

Firefox 5、Opera 11.50、Safari 5、IE 6, 7, 8, 9 では期待通りに動くことを確認しました。

var setPageScrollable = function setPageScrollable( scrollable ) {
    var dv = window;
    var xOffset, yOffset, de;
    if( document.defaultView ) {
        dv = document.defaultView;
        xOffset = dv.pageXOffset;
        yOffset = dv.pageYOffset;
    } else {
        de = document.documentElement;
        xOffset = de.scrollLeft;
        yOffset = de.scrollTop;
    }
    document.documentElement.style.overflow = ( scrollable ? "auto" : "hidden" );
    dv.scrollTo( xOffset, yOffset );
}

サンプルページを置いておきますのでご覧ください。

CSS の position: fixed により描画領域全体を覆う要素を IE 6 でエミュレーションする

Lightbox のように、ブラウザの描画領域全体に何らかの要素を表示したいような場合、おそらくは CSSposition: fixed を指定すると思います。

しかしながら、Internet Explorer 6position: fixed をサポートしていないので、IE6 で同じ事をするには別の方法を採らなければいけません。 IE6 で position: fixed と似たような動きを行わせる方法は、例えば以下のようなものがあります。

自分の web ページ上で使うのであればこの方法でいいと思うのですが、Lightbox のようにライブラリとして配布するものの内部で使うには多少物足りない部分があります。 例えば、指定の要素が position: relative を指定されている要素の子孫である場合、期待する位置に固定されない可能性があります。

そんなわけでもうちょっと詳しく位置計算を行うようなものを考えてみました。

<!-- IE 6 にだけ適用されるように, 条件コメントを使用 -->
<!--[if IE 6]>
<script>
    // 残念ながら HTML 要素に border がついてる場合は fixed とは同じにはならない
    function _ie_getOffsetOfParentFromPageTop( elem ) {
        var e = elem.offsetParent;
        var offset = 0;
        while( e && e.tagName !== "HTML" ) {
            offset += e.clientTop + e.offsetTop;
            e = e.offsetParent;
        }
        return offset;
    }
    function _ie_getOffsetOfParentFromPageLeft( elem ) {
        var e = elem.offsetParent;
        var offset = 0;
        while( e && e.tagName !== "HTML" ) {
            offset += e.clientLeft + e.offsetLeft;
            e = e.offsetParent;
        }
        return offset;
    }
</script>
<style>
    /* border などをつけるときは, それに応じて width と height を調整すること */
    #fixed-box {
        position: absolute;
        top: expression( ( document.documentElement.scrollTop  
              - _ie_getOffsetOfParentFromPageTop( this )  ) + "px" );
        left: expression( ( document.documentElement.scrollLeft 
              - _ie_getOffsetOfParentFromPageLeft( this ) ) + "px" );
        width:  expression( ( document.documentElement.clientWidth )  + "px" );
        height: expression( ( document.documentElement.clientHeight ) + "px" );
    }
</style>
<![endif]-->

サンプルページは以下においてあるので見てみてください。

参考文献

スマートフォンでも見やすい web ページを作るために media queries を使うという選択

最近スマートフォンを使って web ブラウジングしていて気になることがあります。 それは、幅固定の web ページや複数カラムの web ページが非常にみにくい ということ。 当然ながらスマートフォンの画面の横幅は狭いため、PC 用に作られた幅固定や複数カラムのページは端末の画面の横幅に収まりきらずに横スクロールする必要が出てくる可能性があるのです。

皆さんも経験があると思いますが、横スクロールする必要があるページってみにくいですよね。 「幅固定の web ページのレイアウトはやめるべき」 という論調は昔からありました *1 が、PC で見る分には幅固定でもそれほど問題にならない *2 ため、開発者としては作りやすい幅固定の web ページで作ってしまう、という人が多いような気がします。

しかしながら、スマートフォンで見る場合、PC ほど自由が利かない *3 ことが多く、横スクロールが出るページは我慢して見ないといけない、という状況になりかねません。 そんなわけで、スマートフォンでも見やすいページを作るために media queries を使って欲しいなー、という個人的な思いを綴っておきます。

Media Queries とは

Media queries というのは、CSS2 の media types *4 を発展させたものです。 Media types では、スタイルシートを適用するメディアのタイプ (画面表示や印刷など) を指定することができました。 Media queries では、メディアのタイプのほかに画面の横幅や縦幅も指定することができます。 詳しいことは W3C 勧告候補 (Media Queries) や、下記ページなどをご覧ください。

現在のところ、最新の FirefoxOperaSafariChrome で使用することができるようです。

画面幅が小さい場合に 2 段カラムを解除する

Media queries を使って、画面が小さい場合にでも見やすいレイアウトを実現する方法の例を示します。 ここでは下図のように、2 段カラムを考えます。 左カラムがメインの領域で幅は可変、右カラムがサブの領域で、幅は固定です。

左カラムの幅が可変ですので、ある程度画面サイズが小さくなっても横スクロールバーは出現せず、ある程度の見易さを維持できます。 しかしながら、画面サイズをどんどん小さくしていくと、メイン領域である左カラムの方が幅が狭くなってしまい、横スクロールバーは出ていませんがこれはこれでみにくくなってしまいます。

このようなレイアウトを使う場合、media query によって画面幅があるサイズ以下の場合は 2 段カラムを解除するようにすると見やすくなります。 以下に CSS のサンプルを示します。

#test1 { /* 2 つのカラムの親 */
	padding-right: 240px;
}
#test1_1 { /* 左カラム */
	float: left;
	margin-right: -100%;
	width: 100%;
	background-color: #EEEEFF;
}
#test1_2 { /* 右カラム */
	float: right;
	width: 218px;
	margin-right: -236px;
	background-color: #FFEEEE;
}
/* Media queries により, 画面幅が 500px 以下の場合は
 * 2 段カラムを解除する */
@media (max-width: 500px) {
	#test1 {
		padding-right: 3px;
	}
	#test1_1,
	#test1_2 {
		float: none;
		width: auto;
		margin-right: 1px;
	}
}

こうすることで、下図のように画面幅が小さい場合に 2 段カラムを解除できます。

実際のサンプル もご覧ください。

Media Queries を使う理由

画面の幅によってレイアウトを変更する方法はいくつかあると思いますが、media queries には以下のような利点があります。

  • 使いやすい : CSS の仕様として勧告候補になっていますので、CSS との親和性が高くて使いやすいと思います。 直接 CSS ファイルの中に書くこともできますし、HTML の link 要素の属性で指定することもできます。
  • 多くの環境で使用できる : 現在のところ、IE 以外の主要なブラウザではサポートされています。 特にこの記事の主題は 「スマートフォンでも見やすいページを作る」 ということですので、iPhoneAndroid のデフォルトのブラウザで使用できるということは大きいと思います。 また、JavaScript を用いる方法では JavaScript を無効にしていると使えませんが、media queries なら CSS が有効であれば使えます。

記述しやすい、っていうのは結構重要じゃないでしょうか。 例えば 2 段カラムを行うような CSS を書いたとして、そのすぐ下に @media ルールを使って media queries を適用し、指定の画面幅以下の場合は 2 段カラムの CSS を無効にするような CSS を記述できるわけです。 「まあそれぐらいなら書いてもいいかな」 って気分になりませんか?

Media Queries を使いましょう!

というわけで、画面幅が小さい端末でも見やすいページを作るために media queries を使いましょう!

*1:例えば 「横幅を固定するな! − 後悔しないためのWebデザイン」 など。

*2:そもそも PC で見る分には横スクロールする必要がない程度の横幅で web ページのレイアウトがなされますし、ブラウザのウィンドウサイズを小さくしていて横スクロールバーが出てしまう場合はウィンドウサイズを大きくするとか、ユーザースタイルシートを適用するとか、そういう方法で回避可能なわけです。

*3:デフォルトのブラウザ以外は正常に動かないって端末もありますし、ユーザースタイルシートの適用ができないどころか、CSS を適用しないという設定もできないアプリもあります。

*4:詳しくは W3C の勧告候補 (Media types) をご覧ください。

CSS の display: table-cell を使って画面中央に要素を配置する

なんらかの要素を画面の中央に配置したい、ということはしばしばあります。 水平方向 (横方向) の中央揃えは CSStext-align: center を使ったり、ブロック要素ならば margin: auto を使ったりして簡単に実現できますが、垂直方向 (縦方向) の中央揃えはちょっと悩むところです。

昔は table 要素などを使って実現したりしていましたが、表を表示したいわけでもないのに table 要素を使うのはよくないので、ここでは CSS を使って垂直方向の中央揃えを行う方法を説明します。

display: table-cell を使って垂直方向の中央揃えをできるようにする

縦方向の中央揃えをするために CSS を調べていてまず見つけるのは vertical-align プロパティだと思います。 しかしながら、vertical-align プロパティを使って中央揃えを実現しようとしてもおそらくできないはずです。 なぜなら、このプロパティは高さの違うインライン要素を、縦方向のどこで揃えるかを指定するものだからです。

しかしながら、table のセルとなる要素で vertical-align プロパティを使用した場合だけは、中身の要素の垂直方向の配置位置を指定することができます。 すなわち、display: table-cell を指定した要素で vertical-align プロパティを使用すると、その中身の要素の垂直方向の配置位置を指定できます。

<div style="display: table-cell; vertical-align: middle;
height: 200px; background-color: #EEEEEE; padding: 5px;">
<div style="border: solid 1px #666666; padding: 3px;">この要素を中央配置する</div>
</div>

上記のように書くと、下図のように中央配置できていることがわかります。

詳しくは次の記事をご覧ください。

display: table-cell を指定した要素の高さを親要素の高さに揃える

さて、display: table-cell を使えば垂直方向の中央配置ができました。 上で書いた例では、高さ 500px の要素の中に、垂直方向に中央に来るように子要素を置いたわけです。 しかしながら、display: table-cell を指定する要素の高さを、親要素にそろえようとした場合はどうなるでしょうか?

<div style="height: 200px; background-color: #99CCFF;">
<div style="display: table-cell; vertical-align: middle;
height: 100%; background-color: #EEEEEE; padding: 5px;">
<div style="border: solid 1px #666666; padding: 3px;">この要素を中央配置する</div>
</div>
</div>

結果は次のようになってしまいます。

背景色が灰色の要素 (display: table-cell を指定した要素. height: 100% である) の高さが水色の要素の高さと一致して欲しいところですが、期待通りの結果にはなりません。

ではどうすればいいかというと、display: table-cell を指定した要素の親要素に、display: table とすれば良いのです。 そうすれば、高さは一致します。

<div style="display: table; height: 200px; background-color: #99CCFF;">
<div style="display: table-cell; vertical-align: middle;
background-color: #EEEEEE; padding: 5px;">
<div style="border: solid 1px #666666; padding: 3px;">この要素を中央配置する</div>
</div>
</div>

div 要素を画面の中央に配置する例

最後に、div 要素を画面の中央に配置する例を書いておきます。

まず、HTML は次のようにします。 HTML5 の形式で書いています。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" type="text/css" href="1119_vertmiddle.css">
<title>画面中央に要素を配置するサンプル</title>
</head>
<body>
<div id="test">
<p>画面中央に要素を配置</p>
</div>
</body>
</html>

で、この HTML は 1119_vertmiddle.css という CSS ファイルを参照していますが、この CSS ファイルの中身は次のようになります。

html {
height: 100%;
margin: 0 auto; padding: 0;
display: table;
}
body {
min-height: 100%;
margin: 0 auto; padding: 0;
display: table-cell;
vertical-align: middle;
}

#test {
padding: 8px;
border: solid 3px #666666;
border-radius: 8px;
-moz-border-radius: 8px;
-webkit-border-radius: 8px;
}

大事なのは html 要素と body 要素に対するスタイルの適用です。 このように指定しておくと、id が "test" の要素が画面の中央に配置されます。 以下のページのこのサンプルファイルをアップロードしてありますので、ご覧ください。

(ここに書いてある方法は、最近のブラウザ (Firefox 3.6 や Safari 5、Opera 10.6 や IE 8 など) だと問題なく使用できます。)

XHTML における空要素の扱い

HTML 4.01 では内容モデルが EMPTY の要素 (hr 要素や meta 要素など) は以下のように表現していました。

<hr>
XML で言うところの閉じタグを省略した形であり、省略 「できる」 のではなく省略 「しなければならない」 のでした。

では、XHTML ではどのようにすれば良いのでしょうか。 XHTML における空要素、および内容モデルが EMPTY である要素についてメモ代わりに書いておきます。

HTML との互換性を保ったまま XHTML を記述するための指針が XHTML 1.0 の仕様の付録 C にあります。

たとえば <br /> や <hr />, <img src="karen.jpg" alt="Karen" /> といったように、空要素の末尾の / と > との前にスペースを1個組み込むこと。また、たとえば <br /> といったように、空要素には最小化タグ文法を使うこと。これは、XMLで許容されている代わりの文法 <br></br> は、多くの既存のユーザエージェントでは与えられる結果が一定しないからである。
内容モデルが EMPTY ではない要素に空インスタンスが与えられる場合 (たとえば空のタイトルや段落) には、最小化形式は使わないこと (たとえば <p /> は使わず <p></p> を使う)。
つまり、内容モデルが EMPTY の要素は、EmptyElemTag の形式で記述し、それ以外の場合は例え子ノードが何も無くても STagETag を書く、ということです。

XHTML 1.0 Transitional について、内容モデルが EMPTY の要素を以下に挙げておきます。

  • base
  • meta
  • link
  • hr
  • br
  • basefont
  • param
  • img
  • area
  • input
  • isindex
  • col

以上です。

HTML5 では空要素 (void elements) の開始タグを閉じても良い

最近ちょこちょこと HTML5 の草案 を見ているわけですが、ふと気になって HTML 要素の Syntax を見てみたら


Start tags must have the following format:

  1. The first character of a start tag must be a U+003C LESS-THAN SIGN (<).
  2. The next few characters of a start tag must be the element's tag name.
  3. If there are to be any attributes in the next step, there must first be one or more space characters.
  4. Then, the start tag may have a number of attributes, the syntax for which is described below. Attributes may be separated from each other by one or more space characters.
  5. After the attributes, there may be one or more space characters. (Some attributes are required to be followed by a space. See the attributes section below.)
  6. Then, if the element is one of the void elements, or if the element is a foreign element, then there may be a single U+002F SOLIDUS (/) character. This character has no effect on void elements, but on foreign elements it marks the start tag as self-closing.
  7. Finally, start tags must be closed by a U+003E GREATER-THAN SIGN (>) character.

って書いてました。 注目すべきは 6 番の if the element is one of the void elements, or if the element is a foreign element, then there may be a single U+002F SOLIDUS (/) character ってとこ。 個人的に HTML が嫌いな理由の 1 つに空要素 (void elements) を閉じないということがあるから、こういう仕様は嬉しいなぁ。 まあでも XHTML 1.0 からの流れを汲んだらこうなるのは当然なのかな。

HTML5 では (XML 版じゃなくても) 名前空間を使用したりするみたいだし、web 製作者に配慮しつつもかなり XHTML に歩み寄る形になってるなぁ、と思いました。 まだ草案だからこれから変わらないとも限りませんが。

本当の意味での XHTML は世の中に多くないという話

Understanding HTML, XML and XHTML (灯台下暗し -カッターナイフで恐竜を腑分けした記録-) より。

XHTML と HTML がどんな関係にあるのか、それは誤解ばかりが広まっています。WebKit (Safari や S60 Browser のレンダリングエンジン)開発者の Blog で、この誤解を正そうと、下記の記事が書かれました。

要約します。Web から HTTP で取得したテキストで HTML と XHTML を区別するのは HTTP レスポンスヘッダの MIME タイプです。text/html なら HTML 、application/xhtml+xmltext/xml なら XHTML として扱われます。逆に、次に列挙することを行っても XHTML としては扱われません。

  • XHTML の DOCTYPE 宣言を入れる
  • XML 宣言を入れる
  • XHTML 特有の文法に沿った記述を行う
  • XHTML として validate を行う

現状では主に IE が application/xhtml+xmltext/xmlMIME タイプを XHTML として認識しないために XHTML 1.0 を text/html として送信することが多いですが、それを行うと FirefoxSafari などもファイルを奇妙な HTML としてしか扱いません。

まさにそのとおりだなぁ、と思ったり。 FirefoxSafari などもファイルを奇妙な HTML としてしか扱いません というのはちょっと言いすぎな気もするけど。

MIME タイプが "text/html" の場合は、XHTML の文法で書いていたとしても HTML と同じようにしか解析されない、と。 そういうことです。 そんなわけで本当の意味での XHTML というのは MIME タイプが "application/xhtml+xml" や "text/xml" みたいなやつだけなわけですが、残念なことに IE は未だにそれらに対応していないので世の中には XHTML の文法で書かれた HTML があふれかえってるんですよねー。 はぁ、嫌な世の中だ。