JavaScriptでbodyのスクロールを禁止するハンバーガーメニューを作る

こんにちは、ナカムラです。
だいぶ前に階層の深いハンバーガーメニューの作り方をご紹介しました。

tech.arms-soft.co.jp

こちらの記事では開閉の処理について書きました。
今回は、メニューを開いた時のスクロールの制御について書きたいと思います。

今回のポイント

  • メニューを開いている時に、メニューの下のメインコンテンツ(body)をスクロールさせない
  • メニューを閉じた時に、開いた時の表示位置に戻す

DEMO

See the Pen javascriptでbodyのスクロールを禁止するハンバーガーメニューを作る by Nakamura (@takayo-nakamura) on CodePen.

基本の開閉は「階層の深いハンバーガーメニュー」と同じです。
追加した部分を解説していきます。

開く時の処理

まずは現在地を取得します。これは閉じる時にも使いますので変数に入れておきます。
次に、htmlタグにメニューが開いたことを示すクラス「is-menuOpen」を付与します。
CSSの方にis-menuOpenを付与された時のスタイルを追加しています。
positionをfixedにすることで「メニューの下のメインコンテンツ(body)をスクロールさせない」を実現します。

body{  
  .is-menuOpen & {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100vh;
  }
}

ただ、そのままではひとつ問題があります。
ある程度スクロールしたときに、メニューを開くと、一瞬一番上の表示がチラつくようになります。
そのため、開いた時も、スクロール位置を維持するために、
さきほど取得しておいたスクロール位置をbodyのtopへ付与します。
これで開く時のチラツキを解消できます。
(どういう現象かわからない場合は、body.style.top = scrollpos * -1 + 'px'を消して操作してみてください。)

あとはメニューのheightがデフォルトで0なので、
「ウィンドウの高さ - ヘッダーの高さ」を付与することでメニューが開きます。 (ここはCSSで処理しても良いですが、スマホの場合はアドレスバーの高さの考慮なども必要になります)

    // 現在のスクロール位置を取得する
    scrollpos = window.pageYOffset
    // メニューが開いたことを示すクラスをhtmlに付与する
    html.classList.add('is-menuOpen')
    // bodyのtopにスクロール位置を付与する
    document.body.style.top = scrollpos * -1 + 'px'
    // ウィンドウの高さを取得
    bodyHeight = window.innerHeight
    // 取得した高さを、メニューに付与する(ヘッダーの高さを引いた数)
    menu.style.height = bodyHeight - headerHeight + 'px'

閉じる時の処理

閉じる時は、開く時に付与したものをはずしていくイメージです。
is-menuOpenをはずしたので、bodyは通常の状態にもどります。
スクロール位置がずれてしまいますので、window.scrollTo(0, scrollpos)で取得しておいた位置へ移動させます。
最後に、メニューの高さを0にもどせばメニューを閉じる処理の完了です。

    // メニューが開いたことを示すクラスをはずす
    html.classList.remove('is-menuOpen')
    // スクロール位置を開いた時の位置へ戻す
    window.scrollTo(0, scrollpos)
    //メニューを閉じる(高さを0にする)
    menu.style.height = 0

最後に

メニューの階層が深い場合は、「JavaScriptで階層の深いハンバーガーメニューを作る」でご紹介した処理を組み合わせてみてください。
メニューのボリュームによってはメニュー内でスクロールできるように、CSSでoverflow:auto;を追加してください。
ハンバーガーメニューはやることがいっぱいですね。

追記

iOS15以降のアドレスバー問題

スクロールしてアドレスバーが小さくなった時に、bodyの高さがそれに準じない問題があるかと思います。
htmlにmin-height: 100vh;で解決しました。

▼新しい単位で、jsもいらないかも(2022/10/9追記) coliss.com

▼そもそもvhとかいらないかもしれない(2022/11/2追記)
今更ですが、高さいっぱいのメニューなら、bottomも0にすれば勝手に吸着しますよね…vhとかlvhとかいらなかったかもなと思ったりしています。
(後日、記事の内容を見直しますね)

top: 0;
bottom: 0;

CSSで解決する方法

下記の記事で、bodyの固定をせずにスクロールの問題を解決する方法(CSS)が紹介されています。
※safariがまだ対応していないので、結局まだjsが必要そうですが、そのうち対応されるんじゃないでしょうか。

overscroll-behavior: none;

coliss.com

対応状況 caniuse.com

2023/2/2 追記:iOS safariでも機能してました。もうこれでいいですね。