iOS 5 の Safari では、Web 制作にかかわる変更がいくつも入りました。中でも影響が大きかったのが、position: fixed が画面に固定されるようになったことと、overflow: scroll を指定した要素を 1 本の指でスクロールできるようになったことです。どちらも、画面の上や下にメニューを固定した、アプリのような画面を作るのに欠かせない機能でした。ここでは当時の事情と、今の書き方をまとめます。

iOS 5 より前に起きていたこと

それまでの iPhone の Safari では、position: fixed を指定しても、要素が画面に固定されませんでした。Safari は、ページを拡大・縮小しながらその一部を画面に映す仕組みになっていて、fixed の要素は画面ではなく、ページ全体を基準に配置されていたからです。スクロールすると、固定したはずのメニューがページと一緒に流れていきました。

また、overflow: scroll の要素の中身は、2 本の指でなでないとスクロールできず、慣性 (指を離したあとも少し滑り続ける動き) もありませんでした。そのため、JavaScript でタッチの動きを読み取って、スクロールを再現するライブラリがよく使われていました。

iOS 5 では、position: fixed の要素が画面に固定されるようになり、overflow: scroll の要素も 1 本の指でスクロールできるようになりました。さらに -webkit-overflow-scrolling: touch を指定すると、ページ全体と同じ、慣性のあるスクロールになりました。

/* iOS 5 のころの書き方 (今は -webkit-overflow-scrolling は不要) */
.scroll-panel {
  height: 300px;
  overflow: scroll;
  -webkit-overflow-scrolling: touch;
}

今の iOS の Safari では、指定しなくても要素の中が慣性スクロールするので、-webkit-overflow-scrolling は書かなくてかまいません。スクロールする要素の端まで来たときに、ページ全体までスクロールが伝わってしまうのを止めたいときは、overscroll-behavior: contain を使います。

上に固定するなら position: sticky

見出しやメニューを画面の上に固定したいなら、今は position: sticky がまず候補になります。sticky の要素は、ふだんは通常の位置に置かれ、スクロールして指定した位置 (top: 0 など) まで来たところで止まります。

.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background: #fff;
}

fixed と違って通常の流れから外れないので、下の内容が隠れないように余白を足す必要がありません。ただし、祖先の要素に overflow: hidden などが付いていると、その要素を基準に止まろうとするため、ページのスクロールでは止まらなくなります (理由は overflow の基本と float を囲む効果 でも触れています)。また、sticky の要素は、親要素の範囲の中でしか止まりません。

画面の高さは svh・dvh で指定する

スマートフォンのブラウザーでは、アドレスバーなどがスクロールに合わせて出たり隠れたりするので、表示領域の高さが変わります。多くのブラウザーで 100vh はバーが隠れたときの大きいほうの高さになるため、height: 100vh の要素は、バーが出ているときに下が切れて見えます。

そこで、表示領域の状態に合わせた単位が加わりました。

  • svh: バーが出ているときの、小さいほうの高さ
  • lvh: バーが隠れたときの、大きいほうの高さ
  • dvh: その時点の高さ。バーの出し入れに合わせて変わる
.hero {
  min-height: 100vh;  /* 新しい単位に対応していないブラウザー向け */
  min-height: 100svh; /* バーが出ていても、はみ出さない高さ */
}

.app-shell {
  height: 100dvh; /* そのときの表示領域いっぱいにする */
}

dvh はバーの出し入れのたびに値が変わり、レイアウトが動くことがあります。最初の画面をちょうど埋めたいだけなら、svh のほうが落ち着いた表示になります。

画面の端は env(safe-area-inset-*) で避ける

角が丸い画面や、上部に切り欠きのある画面、画面の下にホームバー (ホーム画面に戻る操作のための帯) がある端末では、画面の端の一部が隠れたり、操作と重なったりします。viewport の meta 要素に viewport-fit=cover を指定すると、ページを画面の端まで広げたうえで、避けるべき幅を env(safe-area-inset-*) で CSS から読み取れます。

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
.bottom-bar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  /* ホームバーと重ならないように、下の余白を足す */
  padding: 8px 16px calc(8px + env(safe-area-inset-bottom, 0px));
}

env() の 2 つ目の値は、その値が無い環境で使われる代わりの値です。safe-area-inset-top・safe-area-inset-right・safe-area-inset-left もあり、端末を横に向けたときに、左右の切り欠きを避けるのにも使います。

まとめ

  • iOS 5 で、position: fixed と、要素の中の 1 本指のスクロール (-webkit-overflow-scrolling: touch で慣性つき) が使えるようになった
  • 今は -webkit-overflow-scrolling は不要。上に固定する見出しには position: sticky が使いやすい
  • 画面の高さには、100vh の代わりに svh や dvh を使う
  • viewport-fit=cover と env(safe-area-inset-*) で、切り欠きやホームバーを避ける

参考