🔰はじめての方へ

ハンバーガーメニューの作り方とスクロール対応|CSSだけで作るチェックボックス方式

CSS
記事内に広告が含まれています。
スポンサーリンク

ハンバーガーメニューがスクロールできなかった話

スマホで開くハンバーガーメニュー。

項目が多くなると、メニューの下のほうが画面からはみ出しているのにスクロールできないということが起きます。

そこで、メニューがスクロールできるようにするためのコードを書いてもらいました。

ただ、コードを貼っただけだと「なんで動いたのか」がわからないままです。

なので、よくあるハンバーガーメニューの作り方とあわせて、1行ずつ意味を整理してまとめることにしました!

今回使ったスクロール対応のコードはこちらです。

height: calc(100vh - 72px);
height: calc(100dvh - 72px);
overflow-y: auto;
overscroll-behavior-y: contain;
-webkit-overflow-scrolling: touch;
padding-bottom: calc(30px + env(safe-area-inset-bottom));

この6行の意味を解説する前に、まずはハンバーガーメニューそのものの仕組みから書いていきます。

よくあるハンバーガーメニューの作り方(HTML+CSSのみ)

ハンバーガーメニューの作り方はいくつかありますが、ここではJavaScriptを使わず、HTMLとCSSだけで作るチェックボックス方式を紹介します。

チェックボックスの「チェックが入っている/入っていない」を、メニューの「開いている/閉じている」に見立てる方法です。

HTML

<header class="header">
  <a href="#" class="header-logo">sample</a>

  <input type="checkbox" id="menu-toggle" class="menu-toggle">
  <label for="menu-toggle" class="menu-button" aria-label="メニューを開く">
    <span></span>
    <span></span>
    <span></span>
  </label>

  <nav class="menu">
    <ul>
      <li><a href="#">ホーム</a></li>
      <li><a href="#">サービス</a></li>
      <li><a href="#">料金</a></li>
      <li><a href="#">よくある質問</a></li>
      <li><a href="#">お問い合わせ</a></li>
    </ul>
  </nav>
</header>

ポイントは、input の id と label の for を同じ名前(ここでは menu-toggle)にしていることです。

こうすると、labelをタップしただけでチェックボックスのオン・オフが切り替わります。

label の中の3つの span が、ハンバーガーの三本線になります。

また、あとでCSSで「チェックが入ったらメニューを出す」という指定をするために、input・label・nav は同じ階層に、この順番で並べています。

CSS

/* ヘッダー(画面上に固定) */
.header {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 72px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
  background: #fff;
  box-sizing: border-box;
  z-index: 100;
}

/* チェックボックス本体は見えなくする */
.menu-toggle {
  display: none;
}

/* 三本線のボタン */
.menu-button {
  position: relative;
  width: 30px;
  height: 22px;
  cursor: pointer;
}

.menu-button span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 2px;
  background: #333;
  transition: 0.3s;
}

.menu-button span:nth-child(1) { top: 0; }
.menu-button span:nth-child(2) { top: 10px; }
.menu-button span:nth-child(3) { top: 20px; }

/* メニュー(最初は隠しておく) */
.menu {
  position: fixed;
  top: 72px;
  left: 0;
  width: 100%;
  background: #fff;
  opacity: 0;
  visibility: hidden;
  transition: 0.3s;
}

/* チェックが入ったらメニューを表示 */
.menu-toggle:checked ~ .menu {
  opacity: 1;
  visibility: visible;
}

/* チェックが入ったら三本線を「×」にする */
.menu-toggle:checked + .menu-button span:nth-child(1) {
  top: 10px;
  transform: rotate(45deg);
}
.menu-toggle:checked + .menu-button span:nth-child(2) {
  opacity: 0;
}
.menu-toggle:checked + .menu-button span:nth-child(3) {
  top: 10px;
  transform: rotate(-45deg);
}

開いたり閉じたりする仕組み

開閉を担当しているのは、:checked と、その後ろの ~ や + の記号です。

.menu-toggle:checked は「チェックが入っているときのチェックボックス」という意味です。

~ は「同じ階層で、それより後ろにある要素」、+ は「すぐ隣の要素」を指します。

つまり .menu-toggle:checked ~ .menu は、チェックが入っているときだけ、後ろにあるメニューを表示するという指定になります。

HTMLで input を label や nav より前に置いていたのは、この記号が「後ろにある要素」しか選べないためです。

三本線が「×」に変わるのも同じ仕組みで、1本目と3本目を真ん中に寄せて45度ずつ回転させ、2本目を消しています。

スクロールできなかった原因

上のコードは開閉はきちんと動きますが、メニューの項目が多いと、下のほうが画面からはみ出してしまいます。

原因は、メニューに position: fixed がついていて、高さが決まっていないことです。

position: fixed の要素は画面に貼りついているので、ページをスクロールしても一緒に動きません。

高さが決まっていないと、メニューは中身の分だけ下に伸びていきます。

画面の外にはみ出した部分は、ページのスクロールでも、メニュー自体のスクロールでも見ることができない状態になります。

なので、メニューの高さを画面の中に収めて、はみ出した分はメニューの中でスクロールさせるという直し方になります。

スクロール対応のCSSを1行ずつ解説

ここからは、冒頭の6行を .menu に足したときに、それぞれが何をしているのかを書いていきます。

【1】メニューの高さを画面内に収める(height)

height: calc(100vh - 72px);
height: calc(100dvh - 72px);

メニューの高さを画面の高さからヘッダーの高さ(72px)を引いた値にしています。

メニューはヘッダーのすぐ下(top: 72px)から始まるので、その分を引かないと、下の72px分がまた画面の外にはみ出してしまいます。

同じ height を2回書いているのには理由があります。

vh は画面の高さを基準にした単位ですが、スマホではアドレスバーが隠れたときの大きいほうの高さが基準になるようです。

そのため、アドレスバーが表示されている状態だと、100vh は実際に見えている高さより大きくなり、メニューの下端が隠れてしまいます。

dvh は、アドレスバーが出たり引っ込んだりするのに合わせて、実際に見えている高さに変わってくれる単位です。

調べた範囲では、dvh はChrome 108、Safari 15.4、Firefox 101以降で使えるようになっています。

それより古いブラウザは dvh を理解できないので2行目を読み飛ばし、1行目の vh を使います。

CSSは後から書いたものが優先されるので、この2行は必ず vh → dvh の順番で書きます(逆にすると、対応しているブラウザでも vh で上書きされてしまいます)。

【2】はみ出した分をスクロールさせる(overflow-y)

overflow-y: auto;

中身が高さを超えたときだけ、縦方向にスクロールできるようにする指定です。

今回スクロールできるようになった一番の理由は、この1行と、【1】の高さの指定の組み合わせです。

overflow-y: auto だけ書いても、高さが決まっていないとメニューが中身の分だけ伸びてしまうので、スクロールは発生しません。

高さに上限があって、はじめて「はみ出した分をスクロールする」という動きになります。

【3】後ろのページまで動かないようにする(overscroll-behavior-y)

overscroll-behavior-y: contain;

メニューを一番下までスクロールしきったあとも指を動かし続けると、その勢いが後ろのページに伝わって、ページ本体がスクロールしてしまうことがあります。

これをスクロールチェーンと呼ぶようです。

overscroll-behavior-y: contain をつけると、スクロールがメニューの中だけで止まり、後ろのページに伝わらなくなります。

あわせて、メニューの中で下に引っ張ったときにページが再読み込みされる動きも起きにくくなります。

【4】古いiPhoneでスクロールをなめらかにする(-webkit-overflow-scrolling)

-webkit-overflow-scrolling: touch;

昔のiPhone(iOS 12以前)で、指を離したあとも慣性でスーッと動くスクロールにするための指定です。

iOS 13以降は何も書かなくても慣性スクロールになるので、今はほとんど役目を終えた書き方です。

書いてあっても悪さはしないので、古い端末への保険として残すか削るかは、どちらでも問題ありません。

【5】iPhoneのホームバーに最後の項目がかぶらないようにする(padding-bottom)

padding-bottom: calc(30px + env(safe-area-inset-bottom));

メニューの一番下に余白をつけています。

env(safe-area-inset-bottom) は、iPhoneの画面の下にあるホームバー(横線)の部分の高さを受け取るための書き方です。

30pxにこの高さを足すことで、最後のメニュー項目がホームバーに重なってタップしにくくなるのを防いでいます。

ホームバーがない端末やパソコンでは値が0になるので、ただの30pxの余白として動きます。

スクロール対応を入れた完成版のCSS

基本のCSSの .menu に、ここまでの6行を足すと次のようになります。

変わっているのは .menu の部分だけで、ほかのCSSはそのままです。

.menu {
  position: fixed;
  top: 72px;
  left: 0;
  width: 100%;
  background: #fff;
  opacity: 0;
  visibility: hidden;
  transition: 0.3s;

  /* ここからスクロール対応 */
  height: calc(100vh - 72px);
  height: calc(100dvh - 72px);
  overflow-y: auto;
  overscroll-behavior-y: contain;
  -webkit-overflow-scrolling: touch;
  padding-bottom: calc(30px + env(safe-area-inset-bottom));
  box-sizing: border-box;
}

最後の box-sizing: border-box は、padding を含めた高さで計算させるための指定です。

これがないと、height に padding-bottom の分が上乗せされて、メニューが画面より少し大きくなる可能性があります。

つまずきやすいポイント

72pxはヘッダーの高さに合わせる

コードの中の72pxは、ヘッダーの高さです。 自分のサイトのヘッダーが60pxなら60pxに、というように、height の計算と top の値の両方をそろえておかないと、メニューの下がはみ出したり、すき間ができたりする可能性があります。

ホームバーの余白が効かないことがある

調べた範囲では、env(safe-area-inset-bottom) は、<meta name="viewport"> に viewport-fit=cover を入れたときに値が入る前提で使われていることが多いようです。 ただ、ブラウザや画面の向きによって挙動が違うという報告もあります。 値が0になっていても、30pxの余白は残るので表示が崩れることはありません。

メニューの外を触ると後ろのページが動くことがある

overscroll-behavior が止めてくれるのは、メニューの中でスクロールしきったときの伝わりだけです。 メニューの外側を触ったときにページが動くのを止めたい場合は、メニューが開いている間だけ body のスクロールを止める方法があります。 CSSだけでやるなら body:has(.menu-toggle:checked) { overflow: hidden; } という書き方もあります。

チェックボックス方式はキーボード操作が苦手

display: none にしたチェックボックスは、キーボードのTabキーで選べなくなるようです。 キーボード操作まで考える場合は、JavaScriptで button を使って開閉する作り方もあります。

まとめ

ハンバーガーメニューがスクロールできなかったのは、画面に固定されたメニューの高さが決まっておらず、はみ出した部分を見る手段がなかったからでした。

メニューの高さを画面の中に収めて、overflow-y: auto ではみ出した分をメニューの中でスクロールさせる、というのが直し方の中心です。

残りの行は、アドレスバーやホームバーといったスマホならではの事情に合わせるための調整でした。

コードを書いてもらったあとに1行ずつ意味を追ってみて、どの行が何のために入っているのかがわかるようになりました。

同じようにメニューがスクロールできなくて困っている人に届いたらうれしいです!