【CSS/JS】ハンバーガーメニュー展開時の「背景スクロール禁止」の正しい実装方法と注意点
ハンバーガーメニューやモーダルウィンドウを全画面で展開した際、裏側にあるメインコンテンツがスクロールできてしまうと、ユーザーにとって非常に操作しづらく、不自然なUIになってしまいます。
これを防ぐための「スクロール禁止」処理ですが、実装方法には大きく分けて「CSSで制御する方法」と「JavaScriptのイベントで止める方法」があります。 結論から言うと、基本的にはCSS(overflow: hidden;)を利用して制御するべきです。
今回は、その具体的な実装デモと、なぜJSでスクロールイベントを止めてはいけないのかという明確な理由(デメリット)について解説します!
デモ動作
See the Pen スクロール禁止設定 by mikanbako (@mikanbako) on CodePen.
demo: メニュー展開時の「背景スクロール禁止」
実装コードの紹介
【HTML】
<!-- スクロール確認用のダミーコンテンツ -->
<div class="l-spacer">
<p>↓ スクロールしてみてね ↓</p>
<button class="c-btn js-menu-btn">メニューを開く</button>
</div>
<div class="l-spacer">
<p>↑ 上に戻る ↑</p>
</div>
<!-- 全画面メニュー -->
<nav class="p-menu js-menu">
<div class="p-menu__inner">
<p>全画面メニュー展開中!<br>背景はスクロールできないはずです。</p>
<button class="c-btn js-menu-btn">閉じる</button>
</div>
</nav>
【CSS】
/* スクロール確認用の余白 */
.l-spacer {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 20px;
font-weight: bold;
}
/* ボタンの装飾 */
.c-btn {
padding: 12px 24px;
font-size: 16px;
font-weight: bold;
color: #fff;
background-color: #1bb4d4;
border: none;
border-radius: 8px;
cursor: pointer;
transition: opacity 0.3s ease;
}
.c-btn:hover {
opacity: 0.8;
}
/* 全画面メニュー(初期状態は非表示) */
.p-menu {
position: fixed;
inset: 0;
background-color: rgba(255, 255, 255, 0.95);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
visibility: hidden;
opacity: 0;
transition: all 0.3s ease;
}
.p-menu__inner {
text-align: center;
font-size: 16px;
font-weight: bold;
line-height: 1.6;
}
/* 展開時のメニュースタイル */
.p-menu.is-active {
visibility: visible;
opacity: 1;
}
/* =========================================
スクロール禁止用のクラス
========================================= */
body.is-locked {
overflow: hidden;
}
【JavaScript】
// 要素の取得
const menuBtns = document.querySelectorAll('.js-menu-btn'); // 開く・閉じるボタン両方
const menu = document.querySelector('.js-menu');
const body = document.body;
// メニューの開閉と、bodyのスクロールロックを切り替える関数
const toggleMenu = () => {
// メニュー本体の表示・非表示
menu.classList.toggle('is-active');
// bodyへのスクロール禁止クラスの着脱
body.classList.toggle('is-locked');
};
// 全てのボタンにクリックイベントを付与
menuBtns.forEach(btn => {
btn.addEventListener('click', toggleMenu);
});
コードの解説・こだわったポイント
なぜJSでスクロールイベント(preventDefault)を止めてはいけないのか?
JSの wheel や touchmove イベントを監視して、e.preventDefault() でスクロールを無効化する手法もありますが、以下の2つの大きなデメリットがあるため推奨されません。
① メニュー本体もスクロールできなくなる問題(拡張性の低下)
もし将来、メニューの項目が増えて画面内に収まらなくなった場合、メニューの中身自体もスクロールできなくなってしまいます。
「裏の画面のスクロールは止めたいけど、メニュー本体はスクロールさせたい」という細かい制御をJSだけで書こうとすると、ターゲット要素を判定する条件分岐が非常に複雑になってしまいます。
② パフォーマンスへの悪影響
現在のブラウザは「スクロール時の処理は軽くスムーズに行う(Passive Event Listeners)」という設計が標準になっています。そこにJSで「スクロールを無効にする処理」を無理やり割り込ませると、ブラウザに負荷がかかり、コンソールに警告(Warning)が出たり、動作がカクついたりする原因になってしまいます。
CSSの overflow: hidden; を使うメリット
/* =========================================
スクロール禁止用のクラス
========================================= */
body.is-locked {
overflow: hidden;
}
// メニューの開閉と、bodyのスクロールロックを切り替える関数
const toggleMenu = () => {
// メニュー本体の表示・非表示
menu.classList.toggle('is-active');
// bodyへのスクロール禁止クラスの着脱
body.classList.toggle('is-locked');
};
今回のデモのように、JSの役割はあくまで「body 要素へのクラス(.is-locked)の付け外し」だけに留め、スクロール制御はCSSの overflow: hidden; に任せるのが最も安全な方法です。 これならブラウザへの負荷もかからず、メニューの中身が増えた時もメニュー側(.p-menu)に overflow-y: auto; を追記するだけで簡単にスクロールさせることができます。
補足:iOS Safariにおけるスクロール制御の歴史と現在
Web制作の過去の知識として、古いiOS(iPhone)のSafariブラウザでは body に overflow: hidden; をかけただけでは背景がスクロールできてしまうという有名なバグが存在しました。そのため、以前は position: fixed; を併用したり、専用のJSライブラリ(body-scroll-lock など)を使って無理やり止める複雑なハックが必須とされていました。
しかし、現在のモダンなiOS環境ではこの問題はすでに改善されており、基本的には overflow: hidden; を設定するだけで安全にスクロールを止めることが可能です。
まとめ
今回は、全画面メニュー展開時の「スクロール禁止」の実装方法と、JSによるイベントキャンセルのデメリットについて解説しました。
「とにかく動けばいい」という考え方ではなく、ブラウザのパフォーマンスや、将来メニュー項目が増えた時の保守性(拡張性)まで見据えて実装手法を選ぶことが、フロントエンドエンジニアにとって非常に重要だと思います。
今後もこういった「目に見えない裏側の設計」や「ユーザー体験(UX)」にこだわったコーディングを心掛けていきたいと思います!