結論:背面スクロールは「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多言語化・サーバー選定まで、多言語サイト制作の実績を持つ制作会社が一括対応します。お見積り・ご相談は無料です。
多言語サイト制作の詳細を見る →すぐに相談したい方は お問い合わせフォームへ