🍔

Vanilla.JSで作るハンバーガーメニュー

2024/09/17に公開

ハンバーガーメニューをJSで作ろう

コピペで済ませがちなハンバーガーメニューをVanilla.JSで作りました。

こんなハンバーガーメニューです。

  • ハンバーガーメニューを開いている間、背景は固定
  • 画面幅を検知して、背景固定を解除する
  • スムーズなスクロール
  • ヘッダーの高さぶん自動でずれるページ内リンク

コード

長いので折りたたんでいます。
この下からポイントをしぼって解説しています。

HTML
<header class="header">
      <div class="header__inner">
        <nav class="header__list">
          <ul>
            <li class="header__item"><a href="">HOME</a></li>
            <li class="header__item"><a href="#price">料金</a></li>
            <li class="header__item"><a href="">使い方</a></li>
            <li class="header__item"><a href="">よくある質問</a></li>
            <li class="header__item">
              <a class="header__button" href="">お問い合わせ</a>
            </li>
          </ul>
        </nav>
        <button class="hamburger">
          <span></span>
          <span></span>
          <span></span>
        </button>
      </div>
    </header>
    <div class="l-container">
      <img src="https://placehold.jp/150x400.png" alt="" />

      <p>
        テキストを入れて、メニューを開いた時に背景がスクロールされないかチェックします。
      </p>
      <img src="https://placehold.jp/150x400.png" alt="" />
      <p>
        テキストを入れて、メニューを開いた時に背景がスクロールされないかチェックします。
      </p>
      <h2 id="price" class="price__title">料金</h2>
      <p>
        テキストを入れて、メニューを開いた時に背景がスクロールされないかチェックします。
      </p>
      <img src="https://placehold.jp/150x400.png" alt="" />

      <p>
        テキストを入れて、メニューを開いた時に背景がスクロールされないかチェックします。
      </p>
      <img src="https://placehold.jp/150x400.png" alt="" />
    </div>
CSS
* {
  color: #333;
}

.l-container {
  max-width: 1200px;
  margin: 80px auto 0 auto;
}

.price__title {
  font-size: 36px;
  font-weight: bold;
  text-align: center;
  padding: 40px 0;
}

.header__button {
  padding: 0.5em 1.5em;
  background-color: pink;
  border-radius: 100vw;
  display: block;
}

.header {
  height: 80px;
  width: 100%;
  position: fixed;
  top: 0;
  left: 0;
  margin: 0;
}

.no-scroll {
  overflow: hidden;
}

.header__inner {
  height: inherit;
  background-color: #f5f5f5;
  position: relative;
}

.header__list {
  display: none;
  height: inherit;
  padding: 0 20px;
}
@media screen and (min-width: 1025px) {
  .header__list {
    display: block;
  }
}
.header__list.active {
  background-color: skyblue;
  display: block;
  width: 100%;
  height: 100vh;
  padding: 0;
  position: fixed;
  z-index: 2;
}
@media screen and (max-width: 1024px) {
  .header__list.active ul {
    flex-direction: column;
    align-items: center;
    justify-content: center;
  }
}
.header__list.active .header__button {
  margin-top: 20px;
}
.header__list ul {
  font-size: 20px;
  font-weight: bold;
  height: inherit;
  display: flex;
  gap: 20px;
  align-items: center;
  justify-content: flex-end;
}

.hamburger {
  display: none;
  position: relative;
  cursor: pointer;
  width: 40px;
  height: 40px;
}
@media screen and (max-width: 1024px) {
  .hamburger {
    display: block;
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    right: 20px;
    z-index: 2;
  }
}
.hamburger span {
  display: block;
  position: absolute;
  width: inherit;
  transition: 0.4s ease;
  background-color: #333;
}
.hamburger span:nth-child(1) {
  top: 0;
  right: 0;
  height: 3px;
}
.hamburger span:nth-child(2) {
  height: 3px;
  top: 50%;
  transform: translateY(-50%);
}
.hamburger span:nth-child(3) {
  height: 3px;
  bottom: 0;
}
.hamburger.open span:nth-child(1) {
  transform: rotate(45deg);
  top: 18px;
  right: -7px;
  width: 54px;
}
.hamburger.open span:nth-child(2) {
  display: none;
}
.hamburger.open span:nth-child(3) {
  transform: rotate(-45deg);
  bottom: 18px;
  width: 54px;
  right: -7px;
}
Java Script
// body
const body = document.body;
// ハンバーガーボタン
const hamburger = document.querySelector(".hamburger");
// メニュー
const headerMenu = document.querySelector(".header__list");

// ハンバーガー(三本線のマーク)がクリックされたら
hamburger.addEventListener("click", () => {
  // ヘッダーメニュー・ハンバーガーにactiveクラスをつける
  headerMenu.classList.toggle("active");
  hamburger.classList.toggle("open");
  // 開かれたメニューの後ろをスクロールさせない
  body.classList.toggle("no-scroll");
  // aria-expandedの値を切り替える
  const isExpanded =
    hamburger.getAttribute("aria-expanded") === "true" || false;
  hamburger.setAttribute("aria-expanded", !isExpanded);
});

// メニューのどれかがクリックされたら、開かれたメニューを閉じる
const headerMenuItems = document.querySelectorAll(".header__item");
headerMenuItems.forEach((headerMenuItem) => {
  headerMenuItem.addEventListener("click", () => {
    headerMenu.classList.remove("active");
    body.classList.remove("no-scroll");
  });
});

// 画面幅の変更を検知
window.addEventListener("resize", () => {
  let windowWidth = window.innerWidth;
  if (windowWidth >= 1024) {
    // 画面幅1024px以上でハンバーガーメニューで開かれた背景を解除する・背景固定を解除する
    headerMenu.classList.remove("active");
    body.classList.remove("no-scroll");
    hamburger.setAttribute("aria-expanded", "false"); // ここでリセット
    console.log(`横幅は${windowWidth}pxです。1024pxを超えました。`);
  } else {
    console.log(`横幅は${windowWidth}pxです。1024px未満です。`);
  }
});

// スムーズなスクロール
// ヘッダーの高さ
const headerHeight = document.querySelector(".header").offsetHeight;

// サイト内のa要素を全て取得
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
  anchor.addEventListener("click", (e) => {
    // クリックされた時のデフォルトの動きを防ぐ
    e.preventDefault();
    // クリックされたa要素(リンク)の属性を取得
    const href = anchor.getAttribute("href");
    // href属性の#を除いた部分と一致するIDを取得
    const targetLink = document.getElementById(href.replace("#", ""));
    // ヘッダーの高さ分、スクロール位置を調整する
    const targetPosition =
      targetLink.getBoundingClientRect().top + window.scrollY - headerHeight;
    console.log(
      `上から${
        targetLink.getBoundingClientRect().top
      }pxのところにあるリンク要素までジャンプするためには、今のスクロール量を知りましょう。今は上から${
        window.scrollY
      }pxのところにいて、そこからヘッダーの高さ分${headerHeight}pxを引きます。つまりジャンプ先は上から${targetPosition}pxです。`
    );
    // window.scrollToでスムーズなアニメーションで移動
    window.scrollTo({
      top: targetPosition,
      behavior: "smooth",
    });
  });
});

HTML解説

HTMLは、以下の2つがポイントです。

  • placehold画像を入れて縦に高さを出している(スクロール・ページ内リンク動作確認のため)
  • <li class="header__item"><a href="#price">料金</a></li><h2 id="price" class="price__title">料金</h2>でページ内リンクをしている

CSS解説

パソコンのとき、見せたいものと見えなくしたいものをdisplayで調整しています。

パソコンのとき見せたいもの

  • header__list(HOMEや料金などのメニューリンク)
    • min-width: 1025pxdisplay: block;をして見せている。1024p以下の時はハンバーガーメニューが見えるためheader__listを隠したい。
      .header__list {
      @media screen and (min-width: 1025px) {
      display: block;
      }
      display: none;
      height: inherit;
      

パソコンの時見せたくないもの

  • hamburger(三本線)

画面幅が1024px以下になったら…

画面幅が小さくなったらハンバーガーメニューを見せます。

.hamburger {
  display: none;
  @media screen and (max-width: 1024px) {
    display: block;
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    right: 20px;
    z-index: 2;
  }
  position: relative;
  cursor: pointer;

Java Scriptでclassが追加されたら

ハンバーガーボタンが開かれたらhamburgeropenクラスを、メニューが開いたらheader__listactiveクラスを追加して見た目を調整しています。

SCSSで書くと&.active {と表せるので見やすいです。

.header__list {
  @media screen and (min-width: 1025px) {
    display: block;
  }
  display: none;
  height: inherit;
  padding: 0 20px;
  &.active {
    background-color: skyblue;
    display: block;
    width: 100%;
    height: 100vh;
    padding: 0;
    position: fixed;
    z-index: 2;

Java Script解説

わかりやすさを重視するために、動作ごとにコードを書いています。
constで要素を指定して、クラスをつけ外ししています。

// body
const body = document.body;
// ハンバーガーボタン
const hamburger = document.querySelector(".hamburger");
// メニュー
const headerMenu = document.querySelector(".header__list");

// ハンバーガー(三本線のマーク)がクリックされたら
hamburger.addEventListener("click", () => {
  // ヘッダーメニュー・ハンバーガーにactiveクラスをつける
  headerMenu.classList.toggle("active");
  hamburger.classList.toggle("open");
  // 開かれたメニューの後ろをスクロールさせない
  body.classList.toggle("no-scroll");
  // aria-expandedの値を切り替える
  const isExpanded =
    hamburger.getAttribute("aria-expanded") === "true" || false;
  hamburger.setAttribute("aria-expanded", !isExpanded);
});

メニューが画面いっぱいに開いている間、背景をスクロールさせたくないのでbody.classList.toggle("no-scroll");で背景を固定しています。

アクセシビリティ

aria-expandedは、スクリーンリーダーのためのものです。
ボタンが現在開いているか閉じているかを示すために使います。これを設定すると、スクリーンリーダーがハンバーガーメニューの状態をユーザーに伝えてくれます。

また、メニューの中のなにかがクリックされたら全画面表示をやめて目的の場所へジャンプしたいです。
もしクリックされたらクラスを外す処理を書いてます。

// メニューのどれかがクリックされたら、開かれたメニューを閉じる
const headerMenuItems = document.querySelectorAll(".header__item");
headerMenuItems.forEach((headerMenuItem) => {
  headerMenuItem.addEventListener("click", () => {
    headerMenu.classList.remove("active");
    body.classList.remove("no-scroll");
  });
});

画面幅の検知

レアケースかもしれませんが、小さな画面でメニューを開いたままウィンドウを大きくしたときに1024px以上になったらメニューの表示をPC版にしたいです。
また、メニューが開いている間の背景の固定も解除しないとページがスクロールできなくなるためここでもクラスの削除をしています。

// 画面幅の変更を検知
window.addEventListener("resize", () => {
  let windowWidth = window.innerWidth;
  if (windowWidth >= 1024) {
    // 画面幅1024px以上でハンバーガーメニューで開かれた背景を解除する・背景固定を解除する
    headerMenu.classList.remove("active");
    body.classList.remove("no-scroll");
    hamburger.setAttribute("aria-expanded", "false"); // ここでリセット
    console.log(`横幅は${windowWidth}pxです。1024pxを超えました。`);
  } else {
    console.log(`横幅は${windowWidth}pxです。1024px未満です。`);
  }
});

ヘッダーの高さ分を計算してページ内リンク

ヘッダーの高さをJava Scriptから取得して、ヘッダーにかぶらない位置で遷移します。
console.logでどうやってヘッダーの高さ分ずらしているのかわかりやすく表してみました。

ページを開いた直後であれば、スクロール量は0です。
目的の場所(ここではh2の料金)が画面の一番上から何pxの場所にあるのか?

2つの数字がわかったら、自分が今いる場所からスクロール量を引きます。そこからさらにヘッダーの高さ分(ここでは80px)を引くと、遷移すべきh2の料金までのpx数が出ます。

たとえば・・・
上から953.59375pxのところにあるリンク要素までジャンプするためには、今のスクロール量を知りましょう。今は上から0pxのところにいて、そこからヘッダーの高さ分80pxを引きます。つまりジャンプ先は上から873.59375pxです。

// スムーズなスクロール
// ヘッダーの高さ
const headerHeight = document.querySelector(".header").offsetHeight;

// サイト内のa要素を全て取得
document.querySelectorAll('a[href^="#"]').forEach((anchor) => {
  anchor.addEventListener("click", (e) => {
    // クリックされた時のデフォルトの動きを防ぐ
    e.preventDefault();
    // クリックされたa要素(リンク)の属性を取得
    const href = anchor.getAttribute("href");
    // href属性の#を除いた部分と一致するIDを取得
    const targetLink = document.getElementById(href.replace("#", ""));
    // ヘッダーの高さ分、スクロール位置を調整する
    const targetPosition =
      targetLink.getBoundingClientRect().top + window.scrollY - headerHeight;
    console.log(
      `上から${
        targetLink.getBoundingClientRect().top
      }pxのところにあるリンク要素までジャンプするためには、今のスクロール量を知りましょう。今は上から${
        window.scrollY
      }pxのところにいて、そこからヘッダーの高さ分${headerHeight}pxを引きます。つまりジャンプ先は上から${targetPosition}pxです。`
    );
    // window.scrollToでスムーズなアニメーションで移動
    window.scrollTo({
      top: targetPosition,
      behavior: "smooth",
    });
  });
});

まとめ

ゼロから作ると意外と時間がかかりました。
でも、やっていることはクラスのつけ外しがメインです。
スクロール量の計算はややこしいですが、console.logでどうやって計算しているのかわかって納得感がありました。

Discussion