モーダルを開くと背景がスクロールする問題の正しい止め方|overflow:hiddenが効かない理由・iOSの落とし穴・ガタつき対策まで

モーダルを開くと背景がスクロールする問題の正しい止め方|overflow:hiddenが効かない理由・iOSの落とし穴・ガタつき対策まで

結論:背面スクロールは「body固定+位置復元+ガタつき補正」の3点セットで止める

  • body に overflow:hidden だけではスマホ(特にiOS)で背景が動く。実際にスクロールしているのは body ではないことが多い
  • 確実なのは position:fixed + top:-(現在のスクロール量)px。閉じるときに window.scrollTo で必ず位置を戻す
  • モーダル内側のスクロール領域には overscroll-behavior:contain を付けて、端まで到達したときに背面へ抜けるのを防ぐ
  • PCではスクロールバーが消えてページが約15px右へ動く。scrollbar-gutter か padding-right の補正で潰す
  • 開いている間は背面へ inert を付ける。見えない要素にフォーカスが入る事故が同時に防げる

「スマホでハンバーガーメニューを開くと、後ろのページがスルスル動いてしまう」「PCではモーダルを開いた瞬間にページ全体が横にガタッとずれる」——モーダル・ドロワー・全画面メニューを実装すると、ほぼ必ず通る場所です。結論から言うと、body に overflow: hidden を付けるだけの対処は、スマホでは高い確率で効きません。この記事では、なぜ効かないのかという原因の切り分けから、スクロール位置を失わずに固定する実装、慣性スクロールが背面へ抜けるのを防ぐ指定、そしてPCで起きるガタつきの補正までを、実案件で使っている順番のまま整理します。

なぜ body に overflow:hidden を付けても背景が動くのか

「モーダルを開いたら body に overflow: hidden を付ける」という定番の対処が効かないケースには、はっきりした理由が2つあります。

理由①:スクロールしているのが body ではない

ページ全体のスクロールは、多くの場合 body ではなく html(ルート要素)が担当しています。さらに、レイアウトの都合でラッパー要素に height: 100% と overflow: auto を当てている作りだと、スクロールコンテナはそのラッパーです。この状態で body だけを止めても、実際に動いている要素は止まりません。

どの要素がスクロールしているかは、スクロールイベントの発生源を見るのが一番早いです。ページを軽く動かして、コンソールに出た要素が犯人です。

document.addEventListener('scroll', (e) => {
  const t = e.target === document ? document.scrollingElement : e.target;
  console.log('scrolling:', t.tagName, t.className);
}, true); // キャプチャ段階で拾う=内側のコンテナも検出できる

理由②:iOSのSafariは overflow:hidden を素通りする

iOS Safari は、body の overflow: hidden をページ全体のスクロール抑止として扱ってくれない挙動が長く続いています。指で触ればページは動きます。実測でも、iPhone実機とAndroid実機で結果が割れるのはここが原因で、Androidだけで確認して「止まった」と判断すると本番で必ず出戻ります。動作確認はiOS実機(またはSafariのレスポンシブモード)を必ず含めてください。

確実に止める実装:position:fixed とスクロール位置の復元

環境差に影響されない方法は、開いた瞬間に body を「その場で凍らせる」やり方です。現在のスクロール量を記録し、body を position: fixed にして、記録した量だけ top をマイナスにずらします。見た目は動かないまま、スクロールだけが完全に止まります。

/* CSS */
body.is-locked {
  position: fixed;
  width: 100%;      /* fixed化で幅が縮むのを防ぐ */
  overflow: hidden; /* PC向けの保険 */
}
// JS
let lockedY = 0;

function lockScroll() {
  lockedY = window.scrollY;
  document.body.style.top = `-${lockedY}px`;
  document.body.classList.add('is-locked');
}

function unlockScroll() {
  document.body.classList.remove('is-locked');
  document.body.style.top = '';
  window.scrollTo(0, lockedY); // ← これを忘れると先頭へ吹き飛ぶ
}

最頻の事故:閉じたらページ先頭に戻ってしまう

position: fixed を外した瞬間、ブラウザはスクロール量を覚えていません。閉じる処理で window.scrollTo による復元を書き忘れると、記事の途中で開いた読者が毎回ページ先頭へ飛ばされます。実装バグの中で最も気づかれにくく、離脱に直結する部分です。開く→閉じるを記事の中盤で試して、同じ位置に戻ることを必ず確認してください。

width: 100% を書かないと横幅が狂う

position: fixed にした要素は、幅指定がないと内容に合わせて縮みます。body で起きると、レイアウト全体が一瞬細くなって背景が見えます。width: 100% の1行で防げます。

モーダル内側のスクロールを背面へ抜けさせない

body を固定しても、モーダルの中身が長くて内側にスクロール領域がある場合、その端まで到達したあと指を動かし続けると、慣性が背面へ引き継がれることがあります。これは overscroll-behavior で止めます。

.modal__body {
  max-height: 80dvh;             /* アドレスバー分を含まない実高さ */
  overflow-y: auto;
  overscroll-behavior: contain;  /* 端まで来ても親へ伝播させない */
  -webkit-overflow-scrolling: touch;
}

overscroll-behavior: contain は「このコンテナの中でスクロールを完結させる」という指定で、背面への伝播に加えて、Androidの引っ張って更新(プルトゥリフレッシュ)が誤爆するのも同時に防げます。高さに dvh を使っているのは、スマホのアドレスバーの伸縮で vh が実際の表示領域とずれるためです。この単位の使い分けは、モバイルの高さ指定の記事と同じ考え方になります。

PCで起きる「開いた瞬間に画面が横へずれる」を潰す

Windows版Chromeなどスクロールバーが場所を取る環境では、スクロールを止めた瞬間にスクロールバーが消え、その分だけコンテンツ幅が広がります。実測で約15px、ページ全体が右へガタッと動きます。ヘッダーが中央寄せのサイトだとロゴの位置が動くので、かなり目立ちます。

今どきの環境なら、根本対処は scrollbar-gutter です。最初からバーの分の余白を確保しておくので、消えてもずれません。

html {
  scrollbar-gutter: stable; /* 常にバー幅を確保=消えてもずれない */
}

古い環境も拾う場合は、実際のバー幅を測って固定時に padding で補う方法を併用します。

const sbw = window.innerWidth - document.documentElement.clientWidth;
document.body.style.paddingRight = `${sbw}px`; // 固定するときに付ける
// 解除時に document.body.style.paddingRight = '' で戻す

なお、追従ヘッダーを position: fixed で置いている場合、body への padding は効きません。ヘッダー側にも同じ値の padding-right(または right)を当てる必要があります。ここを揃え忘れると、ヘッダーだけが動きます。

やってはいけない止め方

touchmove を preventDefault で全部殺す

かつての定番でしたが、モーダルの中身までスクロールできなくなるうえ、passive: false のリスナーが常時走るのでスクロール性能も落ちます。overscroll-behavior がある今は選ぶ理由がありません。

html, body に overflow: hidden を常時書く

横スクロールを消す目的で常時 overflow-x: hidden を書いているサイトは多いのですが、これは position: sticky を巻き添えで無効化します。「はみ出しを隠す」対処と「スクロールを止める」対処は別物として扱ってください。横方向のはみ出しは原因要素を特定して個別に潰すのが正解で、この副作用は横スクロールの記事で詳しく扱っています。

閉じたあとの状態も含めてチェックする

背面スクロールの実装は、閉じたあとに壊れているかどうかが見落とされがちです。公開前に次を確認します。

  • 記事の中盤で開いて閉じたとき、同じスクロール位置に戻るか
  • iOS実機で背面が動かないか(Androidだけの確認で終えない)
  • モーダル内側を端まで動かしたあと、指を動かし続けても背面が動かないか
  • PCで開閉したとき、ヘッダーやロゴが横にずれないか
  • 開いている間、Tabキーで背面のリンクへフォーカスが移らないか

最後の項目は inert 属性で解決します。背面のラッパーに inert を付けると、その中の要素はフォーカスもクリックも受け付けなくなります。スクリーンリーダーからも外れるので、アクセシビリティ対応としても正解の形です。

// 開くとき
document.getElementById('page-wrapper').inert = true;
// 閉じるとき
document.getElementById('page-wrapper').inert = false;

まとめ:3点セットで固定し、閉じたときに必ず戻す

背面スクロールの対処は、overflow: hidden の1行で終わらせようとすると必ずどこかの環境で破綻します。①position:fixed とスクロール量の記録で凍らせる、②内側には overscroll-behavior: contain、③スクロールバー分のガタつきを補正する——この3点をセットで入れ、閉じたときの位置復元を必ずテストしてください。実装そのものは20行程度ですが、抜けがあると「記事を読んでいた読者が先頭へ飛ばされる」という、離脱に直結する形で表面化します。

サイト全体の表示品質や、こうしたUI実装の作り込みまで含めて相談したい場合は、多言語サイトの作り方やWordPressのサーバー移行、サーバー比較の記事もあわせてご覧ください。制作や改修のご依頼も承っています。

多言語・ベトナム語サイトの制作でお困りではありませんか?

フォント・文字化け・WordPress多言語化・サーバー選定まで、多言語サイト制作の実績を持つ制作会社が一括対応します。お見積り・ご相談は無料です。

多言語サイト制作の詳細を見る →

すぐに相談したい方は お問い合わせフォームへ

関連記事(コーディング実務Tips)

Web制作Tips(コーディング実務)カテゴリの最新記事