🙄

CSS大解剖 4日目: 「閲覧環境 2/3」

に公開

本稿は、2024年2月頃に書き溜めていたシリーズです。最後まで温存させるのが勿体ないので、未完成ですがそのまま公開します(公開日: 2025/9/21)。そのため、内容の重複や記述方針の不一致があるかもしれませんが、ご理解ください。


CSSの仕様を理解するために、1日ごとにテーマを決めて説明する企画4日目です。今日のテーマは前回に引き続き「閲覧環境」です。

ビューポートとページ

視覚的なブラウザにおいては、文書はビューポートと呼ばれる領域かページと呼ばれる領域に描画されます。

  • ビューポートは液晶ディスプレイのようなデバイスで使われる描画領域です。ビューポートは無限に広い領域の一部を表示しており、ユーザーがスクロール操作をすることで表示位置を変えることができるようになっています。
  • ページ (Working DraftのCSS Paged Media Level 3も参照) は印刷系のデバイスで使われる描画領域です。ページは有限の領域からなり、文書は複数のページに分割して描画されます。

動的なビューポート その1

ビューポートの大きさは動的に変化することがあります。最もわかりやすいのは、Webブラウザのウインドウの大きさをユーザーが変更するようなケースです。スマートフォンのような端末でも、画面分割や画面の回転などでブラウザ画面の大きさが変化する場合があります。

また、ブラウザUIの動的な出し入れによっても見た目上のビューポートが変化しますが、この場合の処理はやや特殊です。以下の「動的なビューポート その2」を参照してください。

初期包含ブロック

ビューポートとページは初期包含ブロック、つまり <html> 要素の包含ブロックの大きさを決定するのに使われます。

  • ビューポートが使われるようなデバイスでは、ビューポートの大きさがそのまま初期包含ブロックの大きさになります。ただし、以下の「動的なビューポート その2」の内容を考慮に入れると、話は少しややこしくなります。
  • ページが使われるようなデバイスでは、最初のページのマージン、ボーダー、パディングを除いたページエリアが初期包含ブロックとして使われます。

初期包含ブロックはおおむね描画領域全体の大きさを表すため、Webページ全体のレイアウト指定で利用したくなることがしばしばあります。このために使えるのがビューポート百分率長さです。名前に反して、これらはビューポートではなく初期包含ブロックからの相対として定義されています。これには以下の亜種がありますが、以下の「動的なビューポート その2」でさらに一般化されます。

  • 物理座標に基づく指定
    • 100vw ... 初期包含ブロックのx軸方向の大きさ
    • 100vh ... 初期包含ブロックのy軸方向の大きさ
  • 論理座標に基づく指定 (Working DraftのCSS Values and Units Level 4 で定義)
    • 100vi ... 初期包含ブロックのインライン軸方向の大きさ
    • 100vd ... 初期包含ブロックのブロック軸方向の大きさ
  • デバイスの向きに応じた指定
    • 100vmin ... 100vw100vh のうち小さいほう。つまり、縦長なら 100vw, 横長なら 100vh
    • 100vmax ... 100vw100vh のうち大きいほう。つまり、縦長なら 100vh, 横長なら 100vw

動的なビューポート その2

ビューポートの大きさが動的に変わりうることは「動的なビューポート その1」で説明しました。しかし、スマートフォン向けブラウザの進歩により、この事情には大きな変化が発生します。

スマートフォンのように面積の制限が厳しいデバイスでは、限りある画面を有効に使うことが非常に重要になります。そこでWebブラウザは、必要性の薄いツールバーをユーザーのナビゲーション操作にあわせて賢く動的に隠すような機能を実装するようになりました。よく見られるのは以下のような実装です。

  • ツールバーはコンテンツの上に存在し、初期状態では表示されている。
  • ユーザーがコンテンツを下にスクロールするときは、コンテンツに関心があると推定されるため、スクロール操作にあわせて徐々にツールバーを隠す。
  • しかし、それだけだとツールバーを再表示する手段がないため、ユーザーがコンテンツを上にスクロールするときは、その操作にあわせて徐々にツールバーを再表示する。上にスクロールするときはコンテンツに興味がなくなったかナビゲーションを行いたいと推測されるので、この推定はある程度合理的だと考えられる。

これにより実質的なビューポートの大きさは動的に変化します。動的に変化するということ自体は以前の挙動と同じですが、この出し入れ操作が今までにない頻度で、ユーザーの無意識下で発生することが問題になります。

従来、ビューポートのリサイズの多くはユーザーの明示的な指示のもとで発生していました。たとえばPCでウインドウの端をドラッグする操作はこれに当たります。このような場合に、ビューポートの変化にあわせてWebページのレイアウトが変化することはそれほど問題にはなりません。しかし、スマートフォンの例ではユーザーは表示中のコンテンツを上下にスクロールしているだけであり、これによりレイアウトがガタガタと変化するのは問題になりえます。

この問題の対応として、「ビューポート」という概念は3種類に細分されることになりました。ここではWorking Draft段階のCSS Values and Units Module Level 4を参照します。

  • 動的なビューポート (dynamic viewport)は従来のビューポートの概念と同様に、ユーザーがコンテンツを見るために実際に使われている領域を指します。動的なビューポートは頻繁に変化するので、動的なビューポートに依存するレイアウトはなるべく避けるのが肝要です。
    • 実際のWebブラウザは、表示の変化にあわせてCSS/JS向けの動的ビューポートサイズをリアルタイムに更新することはせず、1秒程度のデバウンスを行うようです。
  • 大きいビューポート (large viewport)は、動的なビューポートの大きさの上界を与えます。これはスクロール等により頻繁には変化しません。
  • 小さいビューポート (small viewport)は、動的なビューポートの大きさの下界を与えます。これはスクロール等により頻繁には変化しません。

CSSやJSでレイアウトを組むにあたっては、要件にあわせてこれらを使い分けるのが重要になるでしょう。

  • もし、ビューポート内の全ての領域を埋め尽くすようにコンテンツを配置することが重要であり、少しぐらいコンテンツがはみ出てもいいのであれば、大きいビューポートを使うのがいいでしょう。そうすれば、意図せず余白を生じてしまうことはなくなりますし、レイアウトは安定します。
  • 逆に、コンテンツがビューポート内に収まることが重要であり、少しくらい余白が出てもいいのであれば、小さいビューポートを使うのがいいでしょう。そうすれば、コンテンツが大きすぎて画面からはみ出してしまうことはなくなりますし、レイアウトは安定します。
  • どうしても画面の大きさに対してぴったり合わせる必要があるのであれば、動的ビューポートの大きさを参照することも可能です。しかし、ユーザーのスクロールに対して結果がガタガタするなどの弊害が生じるかもしれません。

これまで用いられてきた「ビューポート」の概念は、新しい3種類のビューポートのいずれかに対応づけられます。

  • CSSの単位としての vh/vw/vmin/vmax は、大きいビューポートを参照します。これはすでにこの機能を実装していたWebブラウザの挙動に合わせる形でこのように記述されているものです。なるべく動的ビューポートを参照させないことがユーザーの快適性にとって重要であるため、このような挙動になったのでしょう。
  • 初期包含ブロックは小さいビューポートを参照します。これもWebブラウザの挙動に合わせる形で定義されています。
  • 初期包含ブロックは position: absolute; な要素に影響を与えますが、いっぽう position: fixed; でレイアウトされる要素の位置やサイズ指定は動的なビューポートを参照します。

この新しいビューポート種別を明示するためにCSSの単位が拡張されます。以下はCSS Values and Units Module Level 4で定義されているビューポート相対単位の一覧です。

*w *h *i *b *max *min
v* vw vh vi vb vmax vmin
lv* lvw lvh lvi lvb lvmax lvmin
dv* dvw dvh dvi dvb dvmax dvmin
sv* svw svh svi svb svmax svmin

動的なビューポート その3

スマートフォンのようなデバイスでは、ソフトウェアキーボードもビューポートの大きさに影響を与えます。これはブラウザのツールバーにも似ていますが、以下の違いがあります。

  • 初期状態では表示されない。
  • ツールバーほどの頻度では出し入れされない。

このような性質もあってか、ソフトウェアキーボードのようなUIは動的ビューポートとは別の方法で取り扱われています。

ソフトウェアキーボードの挙動は以下の「モバイルビューポート」の節で紹介する <meta name="viewport"> タグで設定できます。 (これはWorking DraftであるCSS Viewport Module Level 1中に記述されています)

  • interactive-widget=overlays-content ... ソフトウェアキーボードはビューポートの「上」に重ねる形で表示されます。ビューポートの大きさは変わりません。
  • interactive-widget=resizes-content ... ソフトウェアキーボードはビューポートの領域を奪います。ソフトウェアキーボードの大きさ分だけ、ビューポートの大きさが小さくなります。
  • interactive-widget=resizes-visual ... overlays-contentとresizes-contentの中間です。以下で説明する「視覚ビューポート」の大きさのみ縮小します。

非矩形ビューポート

切り欠き (notch) を持つスマートフォンやスマートウォッチなど、四角形以外の表示領域を持つデバイスもあります。このような場合は通常、表示可能な領域全体を覆うようにビューポートが設定されます。このような場合、画像などのコンテンツをビューポート全体に拡大するようにレイアウトすると、コンテンツの一部が欠けてしまうことになります。

このような場合に対応するための仕様がCSS Environment Variables Module Level 1というdraftで策定中です。この仕様では、以下の2つの環境変数を定義することで、非矩形ビューポートでコンテンツ全体を表示するための最適なレイアウトを表現できるようにしています。

  • ビューポートの端の表示不可能な領域を取り除くために必要なマージン量
  • 複数の不連続な領域に分割されている画面における、各々の領域の座標

関連するドラフト仕様として、非矩形ビューポートに関するメディアクエリを定義するCSS Round Display Level 1も策定中です。

モバイルビューポート

スマートフォンのような小さいデバイスで、コンテンツを読みやすいように (物理単位または視角単位にあわせて) 長さを規定すると、画面の幅と高さはかなり小さくなります。

たとえば2012年発売のiPhone 5の画面の横幅は5cm程度です。物理単位を基準にすると、これはおよそ190pxに相当します。また、30cm程度の距離で閲覧することを想定して視角単位を計算すると、これはおよそ450pxに相当します (実際のiPhone 5はこれを物理ピクセルサイズの2倍に丸め、横幅320pxとして計算していたようです)。

しかし、実際にこのような小さい幅でレイアウトを行うと、従来のPC環境を想定したWebサイトの中にはレイアウトが崩れてしまうものも出てきます。そこで、スマートフォン向けWebブラウザでは以下のような対策が取られることになりました。

  • 従来のWebサイトを表示する際は、ビューポートを大きめで計算してレイアウトする。その結果はスマートフォンの画面に収まらないので、ズームアウトするか左右にスクロールして読む。
  • スマートフォン環境に対応したWebサイトはそのことを <meta> タグで明示する。この場合レイアウトは本来のビューポートに基づいて算出される。

このとき使われる <meta> タグは歴史的に2種類あり、HandheldFriendly/MobileOptimized meta tagを使う方法とviewport meta tagを使う方法があります。現在は後者が事実上の標準となっており、こちらの標準化がCSS Viewport Module Level 1で進められています。

ただし、この仕様書の完成度はまだかなり低いので、そこから参照されているA new Device Adaptation spec - QuirksBlogを参考にします。

2つのビューポート

この仕様では、ビューポートが2種類に分割されます。

  • 視覚ビューポート (visual viewport) はスマートフォンの画面に対応する領域です。その大きさは、スマートフォンの本来の画面サイズ × ピンチズーム率 です。
    • ピンチズームとは、レイアウト済みの画面を単純拡大・縮小するようなズームのことで、スマートフォン上のピンチ操作に関連付けられていることが多いです。ピンチズーム以外のズームとしては、デフォルトのフォントサイズを変更するようなズームがよく使われます。
  • レイアウトビューポート (layout viewport) はCSSの指定を解釈してレイアウト処理を行うときに参照される仮想的な領域です。

Webブラウザは、レイアウトビューポートの一部分を切り出して視覚ビューポートに表示することになります。別の言い方をすると、レイアウトビューポートは <html> の上位にもう1つスクロールコンテナを配置する仕組みであるとも言えます。

ビューポートに必要な値

さて、視覚ビューポートとレイアウトビューポートが分離されたことで、Webブラウザは以下の2項目を決定する必要が出てきます。

  • ピンチズーム率 (最大値、最小値、初期値)
  • レイアウトビューポートの大きさ

Viewport Module はこれらの値を決定する方法を規定しています。そのさい、情報源として以下が用いられます。

  • Webブラウザのデフォルトの設定
  • Webサイトの作者からの指示。これには以下の3種類があります。
    • <meta name="viewport"> タグ
    • レガシーで非標準の <meta name="HandheldFriendly"> / <meta name="MobileOptimized"> タグ
    • 非標準の @viewport {} at-rule. これはViewport Moduleの古いバージョンに規定がありましたが、最新ドラフトでは削除されています。

ビューポート引数の解決

Chromiumの実装をもとに、大まかな解決アルゴリズムを説明します。

  1. まず仮のズーム率を決定します。
    • initial-scaleが指定されている場合は、その値を最大/最小でクランプしたものが使われます。
    • initial-scaleがなく、maximum-scaleが指定されている場合は、その値が使われます。
    • いずれもなければ、指定なし (auto) として扱われます。
  2. 次にレイアウトビューポートの大きさを決定します。これはwidth/heightの指定に依存して決まります。
    • width/heightの指定がある場合は、最小値と最大値が以下のルールで解釈されます。
      • 最大値にはwidth/heightに記述された値が使われます。これは以下の2種類に分けられます。
        • 実数指定。この場合はpxで解釈します。
        • device-width / device-height は視覚ビューポートのズーム前のサイズを参照します。
      • 最小値はextend-to-zoomという特別な値とみなされ、 device-width/device-height を仮のズーム値で割って計算されます。つまり、指定した仮のズーム率で視覚ビューポートが埋まりきるようにレイアウトビューポートを決定します。仮のズーム率の指定がなければ最小値の指定はありません。
      • 視覚ビューポートのズーム前のサイズを、これらの制約でクランプした値がレイアウトビューポートになります。最小値と最大値が矛盾する場合は、最小値制約が優先されます。
    • width/heightの指定がなければ制約なし (auto auto) とみなされ、以下のルールで決定されます。
      • もし反対方向の軸のサイズに制約がある場合は、先にそちらを決定します。視覚ビューポートの比率にあわせてレイアウトビューポートの比率を決定します。
      • もし両方の軸の制約がなければ、視覚ビューポートの大きさをそのまま使います。
  3. ズーム率を決定します。もし仮のズーム率の指定があればそれを使います。指定がない場合(auto)は以下のようにレイアウトビューポートの大きさから決定します。
    • initial-scaleが指定されている場合は、その値を最大/最小でクランプしたものが使われます。
    • initial-scaleの指定がなければ、「視覚ビューポートの大きさ / レイアウトビューポートの大きさ」を両辺について計算し、大きいほうの値を使います。つまり、指定したレイアウトビューポートの大きさで視覚ビューポートが埋まりきるようにズーム率を決定します。結果は最大/最小でクランプされます。
  4. 最後に、user-scalableがnoの場合はminimum-scaleとmaximum-scaleを上書きしてズームを禁止します。

たとえば、以下のようなmeta tagを考えます。

<meta name="viewport" content="width=device-width, initial-scale=1">

この場合、初期ズーム率が明示されているので先にズーム率が決まります。大きさは横方向だけ指定されていますが、この指定だと実質的にズーム率に合わせて決定するのと同義になります。ズーム率が1なので、レイアウトビューポートの大きさは視覚ビューポートのズーム前のサイズと同じです。これにより、縦方向の大きさも画面比率から決定され、視覚ビューポートのズーム前のサイズと同じになります。つまり、この指定は勝手にビューポートを大きくしたり、ズームしたりするなという意味になります。 (ユーザーによる明示的なズームは許可されます)

また、以下のようなmeta tagを考えます。

<meta name="viewport" content="width=device-width">

この場合、レイアウトビューポートの大きさが先に決定されます。ズーム率が自動であることから最小サイズの制約がなくなりますが、結果は同じで視覚ビューポートのズーム前のサイズと一致します。結果として初期ズーム率も1になるため、この指定も勝手にビューポートを大きくしたり、ズームしたりするなという意味になります。 (ユーザーによる明示的なズームは許可されます)

最後に以下の指定を考えます。これはChromiumのモバイル向けのデフォルト挙動を非標準の @viewport で表現したものです。

@viewport {
  min-zoom: 0.25;
  max-zoom: 5;

  min-width: 980px;
}

この場合、最大ズームの値である5が仮のズーム値として使われますが、これは続く計算では効果を持ちません。min-widthが固定値で記載されているため、もし視覚ビューポートのズーム前の幅が980px以下であれば、レイアウトビューポートは幅980px以上かつ縦横比を保つように拡大されます。初期ズームはそれに合わせて、0.25倍よりも小さくしない範囲内でなるべく全体を表示するように縮小されます。

もし、 <meta name="viewport"> を指定していなければ、これが使われることになります。

ズーム禁止について

viewport meta tagではminimum-scale/maximum-scaleまたはuser-scalableを用いて、ユーザーによる明示的なズームを禁止することもできます。これはアクセシビリティを低め一般ユーザーにすら迷惑をかけうる欠点こそあれど大した利点はないので、止めておくのが無難でしょう。

Discussion