CSS の overscroll-behavior で iOS のバウンススクロールやモーダル裏のスクロールを止める

モーダルを開いた状態で背景がスクロールしてしまう、ページの端まで来たのにゴムのように引っ張られる――こうした挙動に悩まされた経験はないでしょうか。overscroll-behavior を使えば、スクロールが末端に到達したときの挙動を CSS だけで制御できます。JavaScript でスクロールを無理やり止めていた時代と比べると、はるかにシンプルで確実な方法です。

overscroll-behavior が解決する 2 つの問題

スクロールの末端で起きる厄介な現象は、大きく分けて 2 つあります。

スクロールチェーン

子要素のスクロールが末端に達したとき、スクロール操作が親要素に伝播する現象。モーダル内をスクロールし終わると、裏のページ本体がスクロールし始めるのがこれにあたる。

バウンス効果(オーバースクロール)

ページの最上部や最下部に到達した後もさらにスクロールすると、コンテンツがゴムのように引っ張られて戻る演出。iOS Safari や macOS のスクロールで顕著に見られる。

overscroll-behavior はこの 2 つの現象をそれぞれ個別に、あるいはまとめて制御できます。

基本構文と 3 つの値

overscroll-behavior には 3 つの値があります。

auto(デフォルト)

ブラウザのデフォルト動作をそのまま維持します。スクロールチェーンもバウンス効果もすべて有効です。

contain

スクロールチェーンを止めます。子要素のスクロールが末端に達しても親要素には伝播しません。ただし要素自身のバウンス効果は残ります。

none

スクロールチェーンとバウンス効果の両方を止めます。末端に達するとスクロールが完全に止まり、ゴムのような引っ張り演出も発生しません。

.modal {
  overscroll-behavior: contain;
}

方向を個別に制御したい場合は、overscroll-behavior-x と overscroll-behavior-y を使います。

.horizontal-scroll {
  overscroll-behavior-x: contain;
  overscroll-behavior-y: auto;
}

モーダルの裏スクロールを止める

overscroll-behavior の最も代表的なユースケースが、モーダル表示中の背景スクロール防止です。

HTML
CSS
JavaScript
<div class="scroll-page" id="page">
  <p>背景コンテンツ 1</p>
  <p>背景コンテンツ 2</p>
  <p>背景コンテンツ 3</p>
  <p>背景コンテンツ 4</p>
  <p>背景コンテンツ 5</p>
  <p>背景コンテンツ 6</p>
  <p>背景コンテンツ 7</p>
  <p>背景コンテンツ 8</p>
  <div class="modal-overlay" id="overlay">
    <div class="modal-body">
      <p class="modal-title">モーダル</p>
      <div class="modal-scroll">
        <p>モーダル内のコンテンツ 1</p>
        <p>モーダル内のコンテンツ 2</p>
        <p>モーダル内のコンテンツ 3</p>
        <p>モーダル内のコンテンツ 4</p>
        <p>モーダル内のコンテンツ 5</p>
        <p>モーダル内のコンテンツ 6</p>
        <p>モーダル内のコンテンツ 7</p>
        <p>モーダル内のコンテンツ 8</p>
      </div>
    </div>
  </div>
</div>
.scroll-page {
  height: 180px;
  overflow-y: auto;
  padding: 12px;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  position: relative;
  font-size: 13px;
  color: #374151;
}
.modal-overlay {
  position: absolute;
  inset: 0;
  background: rgba(0,0,0,0.4);
  display: flex;
  align-items: center;
  justify-content: center;
}
.modal-body {
  background: #fff;
  border-radius: 8px;
  padding: 12px;
  width: 80%;
  max-height: 120px;
  display: flex;
  flex-direction: column;
}
.modal-title {
  font-weight: bold;
  font-size: 14px;
  margin: 0 0 8px 0;
}
.modal-scroll {
  overflow-y: auto;
  overscroll-behavior: contain;
  font-size: 12px;
  color: #4b5563;
  flex: 1;
}
.modal-scroll p {
  margin: 0 0 8px 0;
}

モーダル内の .modal-scroll に overscroll-behavior: contain を指定しています。モーダル内を最後までスクロールしても、背景のページがスクロールし始めることはありません。

JavaScript によるスクロール制御との比較

overscroll-behavior が登場する以前は、モーダルの裏スクロールを止めるために JavaScript が必要でした。代表的な手法は body に overflow: hidden を付け外しする方法です。

// モーダルを開くとき
document.body.style.overflow = 'hidden';

// モーダルを閉じるとき
document.body.style.overflow = '';

この方法には複数の問題があります。

body に overflow: hidden を付けるとスクロール位置がリセットされ、ページが最上部に飛ぶことがある
スクロールバーが消えるため、ページの幅が変わってレイアウトがガタつく
iOS Safari では overflow: hidden だけではスクロールを止められないケースがある

overscroll-behavior: contain なら、こうした問題は一切発生しません。body のスタイルを変更する必要がないため、スクロール位置もレイアウトも影響を受けないのです。

JavaScript(overflow: hidden)

body のスタイルを動的に変更するため、スクロール位置のずれやレイアウトシフトの対処が必要。モーダルの開閉に合わせてイベントリスナーの管理も増える。

CSS(overscroll-behavior: contain)

スクロール可能な子要素に 1 行指定するだけで完了。body には一切触れないため、副作用がない。

ページ全体のバウンス効果を止める

Web アプリケーションでネイティブアプリに近い操作感を出したい場合、ページ全体のバウンス効果を無効にしたいことがあります。

html {
  overscroll-behavior: none;
}

html 要素に none を指定すると、ページの端でゴムのように引っ張られる演出が消え、スクロールが端で静かに止まります。ただしこの挙動変更はユーザーの期待を裏切る可能性もあるため、適用範囲は慎重に判断する必要があります。

特に iOS Safari では、バウンス効果は pull-to-refresh(引っ張って更新)のトリガーとしても機能しています。

overscroll-behavior: none でバウンスを止めると、pull-to-refresh も無効になる場合がある。

ブログや情報サイトのようにブラウザネイティブの操作感が求められる場面では、ページ全体への none 指定は避けたほうが無難です。一方、全画面の管理画面やゲーム、地図アプリのように独自のスクロール制御が必要な場面では積極的に活用できます。

横スクロールのカルーセルでの活用

横スクロールのカルーセルは、overscroll-behavior-x が効果を発揮するもう 1 つの典型例です。カルーセルの端までスクロールしたとき、ブラウザバック(macOS Chrome のスワイプによるページ遷移)が発動してしまう問題を防げます。

.carousel {
  overflow-x: auto;
  overscroll-behavior-x: contain;
}

既存記事「Mac 版 Chrome はoverscroll-behavior を none にするとトラックパッドのブラウザバックができなくなる」で触れられているとおり、none を指定するとトラックパッドによるブラウザバック自体が無効になります。カルーセル内だけで止めたい場合は、contain を使うことでカルーセル外のナビゲーション操作を維持できます。

contain はスクロールチェーンのみを止め、ブラウザ独自のナビゲーションジェスチャーは残す。none は両方を止める。

この違いを踏まえると、ほとんどのユースケースでは none よりも contain のほうが安全な選択肢になります。

チャットUIでの実践例

チャット画面のようにメッセージ一覧がスクロールし、その中にさらにコードブロックなどのスクロール可能な要素が入れ子になっている場合も、overscroll-behavior が有効です。

.chat-messages {
  overflow-y: auto;
  overscroll-behavior-y: contain;
  height: 100%;
}

.code-block {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  max-width: 100%;
}

メッセージ一覧をスクロールし終わっても背景ページには伝播せず、コードブロックを横スクロールし終わってもメッセージ一覧の縦スクロールが暴発しません。入れ子のスクロールコンテナそれぞれに contain を付けることで、各要素のスクロールが独立して完結します。

contain と none の使い分け早見表

場面推奨値理由
モーダルの裏スクロール防止containbody に触れず安全
カルーセルの端での伝播防止containブラウザバックを残せる
Web アプリ全体のバウンス無効化noneネイティブ風の操作感
地図・キャンバスの操作領域noneあらゆる伝播を遮断したい

基本方針として、まず contain を試し、それでも不十分な場合にだけ none を使うのがおすすめです。none はブラウザのデフォルト挙動を広く無効化するため、ユーザーが慣れ親しんだ操作感を損なうリスクがあります。

overscroll-behavior はすべてのモダンブラウザでサポートされており、1 行書くだけでスクロールまわりの厄介な問題を解決してくれます。モーダルやカルーセルを実装するときは、真っ先に検討したいプロパティです。