「ハンバーガーメニューを実装したいけど、どこから手をつければいいか分からない」——そんな悩みを抱えたことはありませんか?

この記事では、ライブラリを使わずCSSとJavaScriptだけでハンバーガーメニューを作る方法を、仕組みの要点だけに絞って解説します。完成物は実際に触れる形でGitHub Pagesに公開していますので、まずは動作を確認しながら読み進めてみてください。

実際の動作はこちら

🌐 デモを見る(GitHub Pages)

💻 ソースコード全文(GitHub)

コード全文はGitHubリポジトリで公開しているので、本記事では「どこが動作の核になっているか」を要点だけ抜粋して解説していきます。全部のコードを頭に入れる必要はありません。仕組みを理解して、安心してコピペできるようになることがゴールです。


ハンバーガーメニューで実現できること

このスニペットで作れるのは、スマートフォン向けのシンプルなハンバーガーメニューです。PCでは通常の横並びナビゲーション、スマホではハンバーガーボタンでトグル表示するレスポンシブ対応のヘッダーに仕上がります。

動作の特徴をまとめると次のとおりです。

  • ハンバーガーボタンをクリックするとナビが開き、3本線が×に変形する
  • ナビの外側をクリックすると自動で閉じる
  • Escapeキーを押しても閉じる
  • 画面幅768px以上になると、ハンバーガーボタンが非表示になり通常ナビに切り替わる(スマホを横向きにしたときも対応)
  • aria属性でスクリーンリーダーにも状態を正確に伝える

実際の動きはGitHub Pagesのデモで確認できます。スマホのサイズに近づけたり広げたりしながら触ってみると、このあとの解説が一気に分かりやすくなりますよ。


HTMLの構造を見てみよう

HTMLの肝は「ハンバーガーボタン」と「ナビゲーション」の2つのブロックです。ここでは最小限の抜粋で構造を確認します(ロゴやリンク項目など全要素を含む完全版はGitHubリポジトリで公開しています)。

<!-- ハンバーガーボタン(モバイルのみ表示) -->
<button
  class="c-hamburger"
  type="button"
  aria-label="メニューを開く"
  aria-expanded="false"
  aria-controls="c-nav"
>
  <span class="c-hamburger__line"></span>
  <span class="c-hamburger__line"></span>
  <span class="c-hamburger__line"></span>
</button>

<!-- ナビゲーション -->
<nav class="c-nav" id="c-nav" aria-label="グローバルナビゲーション" aria-hidden="true">
  <ul class="c-nav__list">
    <li class="c-nav__item"><a href="#" class="c-nav__link">TOP</a></li>
    <!-- 他のリンクは省略 -->
  </ul>
</nav>

クラス名のプレフィックスには、FLOCSS(フロックス)というCSS設計ルールを採用しています。c- はComponent(再利用できる部品)を意味します。FLOCSSの詳しい解説は別記事で予定しています。

なぜ <button> タグを使うのか

ハンバーガーボタンには <div> ではなく <button> タグを使います。理由は2つです。

1つ目はキーボード操作への対応です。<button> はデフォルトでキーボードのTabキーでフォーカスでき、Enterキーやスペースキーでクリックできます。<div> にはこの動作がなく、自分で実装が必要になります。

2つ目はスクリーンリーダーへの伝達です。<button> を使うことで、スクリーンリーダー(目の不自由な方が使うツール)が「これはボタンです」と正確に読み上げてくれます。

<div> でクリックイベントだけ付けてしまうのは、よくある実装ミスです。見た目は同じでも、アクセシビリティの面で大きな差が生まれます。

aria属性の役割(初学者向け)

aria属性は、スクリーンリーダーなどの支援技術に対して「この要素が今どんな状態か」を伝えるための属性です。

属性役割
aria-label="メニューを開く"ボタンの名前をスクリーンリーダーに伝える(ボタン内にテキストがないため)
aria-expanded="false"メニューが閉じていることを示す(開いたとき "true" に変わる)
aria-controls="c-nav"このボタンが操作する要素のIDを示す(id="c-nav" と対応)
aria-hidden="true"(ナビ側)非表示のとき「この要素は読まなくていい」と伝える

最初は難しく感じるかもしれませんが、このスニペットをコピペするだけでアクセシビリティ対応ができます。慣れてきたら「どうしてこう書くのか」を一つずつ確認してみてください。


CSSで3本線とアニメーションを実装する

CSS全文は長いため、GitHubリポジトリのほうに譲り、ここでは「動きの核」になるポイントだけを抜粋します。

×(バツ)変形アニメーションの仕組み

ハンバーガーボタンのアニメーションは、3本線それぞれに transform をかけて×形に組み替えるシンプルな作りです。

/* 1本目: 下に7px下がって45度回転 */
.c-hamburger.is-active .c-hamburger__line:nth-child(1) {
  transform: translateY(7px) rotate(45deg);
}

/* 2本目: 非表示 */
.c-hamburger.is-active .c-hamburger__line:nth-child(2) {
  opacity: 0;
}

/* 3本目: 上に7px上がって -45度回転 */
.c-hamburger.is-active .c-hamburger__line:nth-child(3) {
  transform: translateY(-7px) rotate(-45deg);
}

translateY(7px) という数値には根拠があります。線の高さ(height: 2px)と、線間のギャップ(gap: 5px)を足すと 2 + 5 = 7px になります。この距離だけ上下に動かすことで、1本目と3本目が中央の線の位置に重なり、×形になる、という算数です。

transform-origin: center(全体CSSに記載)は変形の起点を「中心」にする設定です。これがないと、線が端を軸に回転してしまって、ずれた見た目になります。

ナビのPC・スマホ切り替え

スマホではナビを position: absolute でヘッダー下に重ね、PCでは position: static に戻して横並びに戻します。

.c-nav {
  display: none;
  position: absolute; /* スマホ: ヘッダー下に重ねて表示 */
  top: var(--header-height-sp);
  left: 0;
  width: 100%;
}

.c-nav.is-open {
  display: block;
}

@media (min-width: 768px) {
  .c-nav {
    display: block;
    position: static; /* PC: absoluteを打ち消す */
  }
}

注目は position: static で打ち消している点です。これを忘れると、PCでナビがヘッダーから飛び出したレイアウトになってしまいます。よくあるハマりポイントです。

論理プロパティ(margin-block / padding-inline)について

CSS全文を覗くと margin-blockpadding-inline といった見慣れない書き方が出てきます。これは「論理プロパティ」と呼ばれる比較的新しいCSSの書き方で、従来の margin-top / margin-bottompadding-left / padding-right の一括指定に相当します。横書きの日本語サイトであれば、動作は従来のプロパティと同じです。「最近のコードでよく見かける書き方」として覚えておけば問題ありません。


JavaScriptでトグル動作を実装する

JavaScriptは役割ごとに関数を分けて書いています。ここでは核となる openMenu / closeMenu を抜粋します。イベント登録や即時関数でのスコープ囲みといった定型処理はGitHubリポジトリのほうをご確認ください。

メニューを開く・閉じる関数

function openMenu() {
  hamburger.classList.add("is-active");
  nav.classList.add("is-open");
  hamburger.setAttribute("aria-expanded", "true");
  hamburger.setAttribute("aria-label", "メニューを閉じる");
  nav.setAttribute("aria-hidden", "false");
}

function closeMenu() {
  hamburger.classList.remove("is-active");
  nav.classList.remove("is-open");
  hamburger.setAttribute("aria-expanded", "false");
  hamburger.setAttribute("aria-label", "メニューを開く");
  nav.setAttribute("aria-hidden", "true");
}

openMenucloseMenu を別関数にしている理由は、「外部クリックで閉じる」「Escapeキーで閉じる」など、複数の場所から closeMenu() を呼び出す必要があるためです。まとめておくことで、コードの重複をなくせます。

ポイントは、クラスの切り替えだけでなくaria属性もセットで更新すること。見た目だけでなく、スクリーンリーダーに対しても「今メニューが開いている/閉じている」を正確に伝えるのが、このコードの肝です。

ナビ外クリック・Escapeキー・リサイズ対応

3つの閉じるイベントは、どれも最終的に closeMenu() を呼ぶだけの作りになっています。

  • ナビ外クリック: document にクリックイベントを付けて、hamburger.contains(e.target)nav.contains(e.target) の両方がfalseのときだけ閉じる
  • Escapeキー: document.addEventListener("keydown", ...)e.key === "Escape" を検知。閉じた後は hamburger.focus() でフォーカスをボタンに戻す
  • リサイズ対応: window.matchMedia("(min-width: 768px)") で画面幅の変化を検知し、PCサイズに広がったタイミングでドロワーが開きっぱなしになるのを防ぐ

コードの具体的な書き方はGitHubscript.js を見てみてください。いずれも20〜30行程度のシンプルなイベントハンドラです。

「閉じた後に hamburger.focus() を呼ぶ」というのは小さなポイントですが、キーボード操作のユーザーにとっては「フォーカスが迷子になる」のを防ぐ大事な配慮です。押したボタンのところにフォーカスを戻す、という自然な流れを意識しています。


コピペで使えるソースコード全文

HTML・CSS・JSの全文は、GitHubリポジトリで公開しています。そのままコピペして使える状態で置いていますので、手元のプロジェクトに取り込んでお使いください。

コピペ用:HTML / CSS / JS 全文

ここからコピペで使える完全なソースコードを掲載しています。GitHubを使わない方はこちらをそのままコピーしてください。HTML・CSS・JSの3ファイル構成で、そのまま動作します。

HTML(index.html)

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>001_hamburger-basic</title>
    <link rel="stylesheet" href="./assets/css/style.css" />
  </head>
  <body>
    <header class="c-header">
      <div class="c-header__inner">
        <!-- ロゴ -->
        <a href="#" class="c-header__logo">LOGO</a>

        <!-- ハンバーガーボタン(モバイルのみ表示) -->
        <button
          class="c-hamburger"
          type="button"
          aria-label="メニューを開く"
          aria-expanded="false"
          aria-controls="c-nav"
        >
          <span class="c-hamburger__line"></span>
          <span class="c-hamburger__line"></span>
          <span class="c-hamburger__line"></span>
        </button>

        <!-- ナビゲーション -->
        <nav class="c-nav" id="c-nav" aria-label="グローバルナビゲーション" aria-hidden="true">
          <ul class="c-nav__list">
            <li class="c-nav__item"><a href="#" class="c-nav__link">TOP</a></li>
            <li class="c-nav__item"><a href="#" class="c-nav__link">ABOUT</a></li>
            <li class="c-nav__item"><a href="#" class="c-nav__link">WORKS</a></li>
            <li class="c-nav__item"><a href="#" class="c-nav__link">BLOG</a></li>
            <li class="c-nav__item"><a href="#" class="c-nav__link">CONTACT</a></li>
          </ul>
        </nav>
      </div>
    </header>

    <script src="./assets/js/script.js" defer></script>
  </body>
</html>

CSS(assets/css/style.css)

/* ================================================
   カスタムプロパティ
   ================================================ */
:root {
  --header-height-sp: 60px;
  --header-height-pc: 72px;

  /* ===== z-index 階層管理 ===== */
  --z-header: 20; /* ヘッダー背景 */
}

/* ================================================
   c-header: ヘッダー全体
   ================================================ */
.c-header {
  position: sticky;
  top: 0;
  z-index: var(--z-header);
  background-color: #fff;
  border-bottom: 1px solid #e0e0e0;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
}

.c-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  max-width: calc(1200px + 20px * 2);
  margin-inline: auto;
  padding-inline: 20px;
  height: var(--header-height-sp);
}

@media (min-width: 768px) {
  .c-header__inner {
    padding-inline: 40px;
    height: var(--header-height-pc);
  }
}

.c-header__logo {
  font-size: 20px;
  font-weight: bold;
  color: #333;
  text-decoration: none;
  letter-spacing: 0.05em;
}

@media (min-width: 768px) {
  .c-header__logo {
    font-size: 24px;
  }
}

/* ================================================
   c-hamburger: ハンバーガーボタン
   ================================================ */
.c-hamburger {
  /* ボタンリセット */
  appearance: none;
  background: none;
  border: none;
  cursor: pointer;
  padding: 8px;
  margin: -8px;

  /* 3本線を縦に並べるレイアウト */
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 5px;
  width: 44px;
  height: 44px;
}

@media (min-width: 768px) {
  /* PCでは非表示 */
  .c-hamburger {
    display: none;
  }
}

/* ハンバーガーの1本線 */
.c-hamburger__line {
  display: block;
  width: 24px;
  height: 2px;
  background-color: #333;
  border-radius: 2px;
  transform-origin: center;
  transition:
    transform 0.3s ease,
    opacity 0.3s ease;
}

/* ×(バツ)アニメーション */
/* is-active クラスが付いたとき、3本線 → ×に変形 */

/* 1本目: 右上がりの斜線 */
.c-hamburger.is-active .c-hamburger__line:nth-child(1) {
  /* 線の高さ(2px) + gap(5px) = 7px(中央線との距離) */
  transform: translateY(7px) rotate(45deg);
}

/* 2本目: 非表示 */
.c-hamburger.is-active .c-hamburger__line:nth-child(2) {
  opacity: 0;
}

/* 3本目: 右下がりの斜線 */
.c-hamburger.is-active .c-hamburger__line:nth-child(3) {
  /* 線の高さ(2px) + gap(5px) = 7px(中央線との距離) */
  transform: translateY(-7px) rotate(-45deg);
}

/* ================================================
   c-nav: ナビゲーション
   ================================================ */
.c-nav {
  /* モバイル: 非表示(デフォルト) */
  display: none;
  position: absolute;
  top: var(--header-height-sp); /* .c-header の高さに合わせる */
  left: 0;
  width: 100%;
  background-color: #fff;
  border-bottom: 1px solid #e0e0e0;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.06);
}

@media (min-width: 768px) {
  /* PC: 常に表示(横並び) */
  .c-nav {
    display: block;
    position: static;
    width: auto;
    background: none;
    border: none;
    box-shadow: none;
  }
}

/* モバイル: is-open クラスで表示 */
.c-nav.is-open {
  display: block;
}

.c-nav__list {
  list-style: none;
  margin: 0;
  padding: 0;
}

@media (min-width: 768px) {
  /* PC: 横並び */
  .c-nav__list {
    display: flex;
    gap: 32px;
  }
}

/* モバイル: 区切り線 */
.c-nav__item {
  border-bottom: 1px solid #f0f0f0;
}

.c-nav__item:last-child {
  border-bottom: none;
}

@media (min-width: 768px) {
  /* PC: 区切り線なし */
  .c-nav__item {
    border: none;
  }
}

.c-nav__link {
  display: block;
  padding: 16px 20px;
  color: #333;
  text-decoration: none;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.05em;
  transition: color 0.2s ease;
}

/* マウス系デバイスかつホバー操作が可能な場合のみ適用(タッチデバイス・ハイブリッドのタッチ操作を除外) */
@media (any-hover: hover) and (pointer: fine) {
  .c-nav__link:hover {
    color: #0066cc;
  }
}

@media (min-width: 768px) {
  /* PC: パディング調整 */
  .c-nav__link {
    padding: 0;
    font-size: 14px;
  }
}

JavaScript(assets/js/script.js)

/**
 * 001_hamburger-basic
 * シンプルなハンバーガーメニュー
 *
 * 機能:
 * - ハンバーガーボタンクリックでナビゲーションをトグル表示
 * - ボタンに is-active、ナビに is-open クラスを付与/除去
 * - aria-expanded 属性を連動更新(アクセシビリティ対応)
 * - ナビ外クリックで閉じる
 * - Escape キーで閉じる
 */

(function () {
  "use strict";

  // 要素取得
  const hamburger = document.querySelector(".c-hamburger");
  const nav = document.querySelector(".c-nav");

  // 要素が存在しない場合は処理しない
  if (!hamburger || !nav) return;

  /**
   * メニューを開く
   */
  function openMenu() {
    hamburger.classList.add("is-active");
    nav.classList.add("is-open");
    hamburger.setAttribute("aria-expanded", "true");
    hamburger.setAttribute("aria-label", "メニューを閉じる");
    nav.setAttribute("aria-hidden", "false");
  }

  /**
   * メニューを閉じる
   */
  function closeMenu() {
    hamburger.classList.remove("is-active");
    nav.classList.remove("is-open");
    hamburger.setAttribute("aria-expanded", "false");
    hamburger.setAttribute("aria-label", "メニューを開く");
    nav.setAttribute("aria-hidden", "true");
  }

  /**
   * メニューのトグル
   */
  function toggleMenu() {
    const isOpen = hamburger.classList.contains("is-active");
    if (isOpen) {
      closeMenu();
    } else {
      openMenu();
    }
  }

  // ハンバーガーボタンのクリックイベント
  hamburger.addEventListener("click", toggleMenu);

  // ナビ外クリックで閉じる
  document.addEventListener("click", function (e) {
    const isOpen = nav.classList.contains("is-open");
    if (!isOpen) return;

    // クリック対象がハンバーガーまたはナビの内側でなければ閉じる
    const isOutside = !hamburger.contains(e.target) && !nav.contains(e.target);
    if (isOutside) {
      closeMenu();
    }
  });

  // Escape キーで閉じる
  document.addEventListener("keydown", function (e) {
    if (e.key === "Escape" && nav.classList.contains("is-open")) {
      closeMenu();
      // フォーカスをハンバーガーボタンに戻す
      hamburger.focus();
    }
  });

  // 画面幅が768px以上になったらナビを閉じる(縦横回転・リサイズ対応)
  const mediaQuery = window.matchMedia("(min-width: 768px)");
  mediaQuery.addEventListener("change", function (e) {
    if (e.matches) {
      // PCサイズ: ナビは常に表示されるため aria-hidden を除去
      if (nav.classList.contains("is-open")) {
        closeMenu();
      }
      nav.removeAttribute("aria-hidden");
    } else {
      // SPサイズに戻ったとき: ナビは非表示状態なので aria-hidden を付与
      nav.setAttribute("aria-hidden", "true");
    }
  });
})();

カスタマイズポイント

コピペしたあと、自分のサイトに合わせて調整したい箇所を紹介します。

  • ブレークポイント: 768px を変更したい場合は、CSSのメディアクエリとJSの matchMedia("(min-width: 768px)") を同じ値に揃えて変更する
  • ヘッダーの高さ: :root--header-height-sp--header-height-pc を変更するだけでOK。ナビの表示位置も自動で追従する
  • アニメーションの速度: .c-hamburger__linetransition の秒数を変更する(例: 0.3s0.5s
  • 線の色・太さ: .c-hamburger__linebackground-colorheight を変更する
  • ホバー時の文字色: .c-nav__link:hovercolor を変更する

まとめ

この記事では、CSSとJavaScriptだけで作るハンバーガーメニューの実装を解説しました。

  • <button> タグを使うことで、キーボード操作とスクリーンリーダーへの対応が自動的にできる
  • aria-expandedaria-hidden をJSで連動させることで、スクリーンリーダーが正確に状態を読み上げてくれる
  • ×変形アニメーションの translateY(7px) は「線の高さ + gap」の計算から来ている
  • matchMedia でリサイズや画面回転にも対応できる

私自身、最初にハンバーガーメニューを実装したとき、<button> ではなく <div> でクリックイベントを付けてしまいました。見た目は同じに動いていたので気づかず、後からアクセシビリティのチェックで指摘されて慌てて修正した記憶があります。学び始めたころは「動けばいい」と思いがちでしたが、今思えばあの詰まり方が一番の学びでした。

まずはGitHub Pagesのデモで動きを確認して、気に入ったらGitHubリポジトリからコードを持ち帰ってみてください。動かしながら「この部分は何をしているのか」を一つずつ確認すると、理解が一気に深まりますよ。

ナビゲーションメニューの実装パターンはほかにも色々あります。このシンプルな実装をしっかり理解できたら、次は一歩進んだドロワー型(オーバーレイ・スクロールロック・フォーカストラップ付き)にも挑戦してみましょう。


関連記事

【関連記事】ナビゲーションメニュー実装パターン5選
→ 内部リンク(まとめ記事が公開されたら設置)