【CSS/JS】ハンバーガーメニュー展開時の「背景スクロール禁止」の正しい実装方法と注意点

ハンバーガーメニューやモーダルウィンドウを全画面で展開した際、裏側にあるメインコンテンツがスクロールできてしまうと、ユーザーにとって非常に操作しづらく、不自然なUIになってしまいます。

これを防ぐための「スクロール禁止」処理ですが、実装方法には大きく分けて「CSSで制御する方法」と「JavaScriptのイベントで止める方法」があります。 結論から言うと、基本的にはCSS(overflow: hidden;)を利用して制御するべきです。

今回は、その具体的な実装デモと、なぜJSでスクロールイベントを止めてはいけないのかという明確な理由(デメリット)について解説します!

デモ動作

See the Pen スクロール禁止設定 by mikanbako (@mikanbako) on CodePen.

全画面でのデモ動作は以下のリンクからご確認下さい!(別タブで開かれます)

実装コードの紹介

【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の wheeltouchmove イベントを監視して、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ブラウザでは bodyoverflow: hidden; をかけただけでは背景がスクロールできてしまうという有名なバグが存在しました。そのため、以前は position: fixed; を併用したり、専用のJSライブラリ(body-scroll-lock など)を使って無理やり止める複雑なハックが必須とされていました。
しかし、現在のモダンなiOS環境ではこの問題はすでに改善されており、基本的には overflow: hidden; を設定するだけで安全にスクロールを止めることが可能です。

まとめ

今回は、全画面メニュー展開時の「スクロール禁止」の実装方法と、JSによるイベントキャンセルのデメリットについて解説しました。

「とにかく動けばいい」という考え方ではなく、ブラウザのパフォーマンスや、将来メニュー項目が増えた時の保守性(拡張性)まで見据えて実装手法を選ぶことが、フロントエンドエンジニアにとって非常に重要だと思います。

今後もこういった「目に見えない裏側の設計」や「ユーザー体験(UX)」にこだわったコーディングを心掛けていきたいと思います!

Prev
Next