JavaScript

PCはメガメニュー/スマホはハンバーガー+アコーディオン|JavaScript実装ガイド

みなと

Webサイトのグローバルナビでは、PCとスマホで異なるメニュー構成を採用するのが一般的です。例えば、以下のような構成は企業サイトやサービスサイトでもよく見かけます。

PC:メガメニューで情報を一覧しやすく
スマホ:ハンバーガーメニュー+アコーディオンでコンパクトに表示

プラグインやライブラリを使えば、こうしたメニューも比較的簡単に導入できます。しかし、実務では次のような “細かい仕様の違い” に直面することが少なくありません。

  • 「デザイナーから指定された独自アニメーションを再現したい」
  • 「オーバーレイの出し方を他のコンテンツと連動させたい」
  • 「開いているときに別の場所をクリックしたら閉じる挙動を細かく制御したい」

こういった場面では、既存プラグインだけでは思い通りにならないケースも多いです。

自作できると、実務で強い

だからこそ、基本の構造と動きを 自分で実装できる力 があると強いです。自作できれば、プロジェクトごとの細かい仕様変更にも柔軟に対応できますし、ライブラリに依存しないシンプルなコードで運用もしやすくなります。

この記事では、以下のような構成を HTML・CSS・JavaScript(バニラJS)だけで実装する方法を紹介します

PC → メガメニュー(ドロップダウン型)
スマホ → ハンバーガーメニュー+アコーディオン展開

まずは完成デモを見てから、コードと実装のポイントを丁寧に解説していきましょう!

メガメニューとは?

メガメニュー(Mega Menu)とは、ナビゲーションの下に大きなドロップダウンを表示し、多くのリンクや情報を整理して見せるUIパターンのことです。企業サイトや大学サイト、ECサイトなど、情報量が多いサイトでよく使われます。

この記事では、以下のようなPC表示時のメガメニューを題材に解説を進めます。

メガメニューのPC表示例。1つ目のメニューをクリックして展開した状態
PC表示で1つ目のメニューを開いた状態の例(デモページより)

中身は今回はシンプルに、サムネ+テキストリンクを並べているだけです。ここはプロジェクトごとに自由に組み替えたり、デザインをリッチにしたりできます。

呼び方はいろいろ

「メガメニュー」という呼び方が一般的ですが、他にも以下のような呼び方が使われることがあります。

  • メガドロップ(Mega Drop)
  • メガナビ(Mega Navigation) など

呼び方に明確な定義はなく、ほぼ同じものを指して使われています。本記事では「メガメニュー」で統一します。

デザインも動きも多彩

一言で「メガメニュー」といっても、その構成や見せ方には多彩なパターンがあります。

まず、構成面では

  • トリガー:ホバー/クリック
  • 表示位置:ヘッダー直下/全画面
  • レイアウト:情報量やデザインによって千差万別
  • 背景:オーバーレイ・ぼかし・透過 など
  • レスポンシブ:スマホでは構成を変更

さらに、表示演出にもいろんな違いがあります

  • スライドやフェードイン、上から降りてくるタイプ
  • ユニークなアニメーションを使うタイプ
  • 表示のタイミング(即時/遅延)やアイコンの動きなど細部の工夫

このように、メガメニューは構造も演出も実にバリエーション豊富です。だからこそ、基本の仕組みを自分で実装できると、さまざまな仕様に柔軟に対応できるようになります。

PCとスマホで変える!よくある構成パターン

メガメニューは、PCとスマホで見せ方や構成を切り替えるケースがとても多いです。これは、両者で画面サイズや表示領域の制約が大きく異なるため、それぞれに合わせた最適な見せ方にする必要があるからです。

よくある構成パターンのひとつが、
PCではメガメニュー
スマホではハンバーガー+アコーディオン
という組み合わせです。

PC向けのメガメニューは情報量が多くなりがちですが、この構成ならスマホでも内容をコンパクトに収めやすく、実装も比較的シンプルです。今回のデモでは、この基本パターンをベースに、ひとつのHTML構造をPCとスマホの両方で使いまわし、CSSとJavaScriptで表示や挙動を切り替える形にしています。まずはこの「基本形」を押さえておくと、応用もしやすくなりますよ。

みなと
みなと

PCとスマホで掲載する情報が同じなら、HTMLはひとつにまとめた方が保守がラクです。修正や更新も一箇所で済みますからね。ただ、PCとスマホで見せたい情報がけっこう違う場合もあります。そういうときは、別々に組むしかないケースも多いです。

完成デモの紹介

ここまでで、メガメニューの基本やPC/スマホの構成パターンを見てきました。それでは実際に、今回の記事の核となる完成デモを見てみましょう

メガメニューとハンバーガーメニューの実装デモページ(PC表示)のスクリーンショット

このデモでは、PCではメガメニュー、スマホではハンバーガー+アコーディオンという構成を、JavaScriptでシンプルに実装しています。余計な装飾や複雑な機能は加えず、基本の仕組みを理解することを重視した内容です。

PC・スマホどちらの挙動も確認できるので、画面幅を切り替えながらチェックしてみてください。

コード全体(HTML / CSS / JavaScript)

ここでは、今回のデモで使用したHTML/CSS/JavaScriptのコード全体を紹介します。全体像をざっと確認したうえで、後のセクションでポイントを順に解説していきます。

HTMLコード

メガメニューとハンバーガーメニューの両方を、ひとつのHTML構造の中で実装しています。PC・スマホで表示方法が切り替わるよう、シンプルかつ汎用的なマークアップにしています。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>メガメニューの実装デモ</title>
  </head>
  <body>
    <div class="wrapper">
      <!-- ============================
        ヘッダー
      ============================ -->
      <header class="header">
        <!-- ロゴ(左側固定リンク) -->
        <div class="header-logo"><a href="#">SAMPLE</a></div>

        <!-- ハンバーガーメニュー(SP表示時のみ) -->
        <div class="header-hamburger"><button type="button"><span></span><span></span><span></span></button></div>

        <!-- グローバルナビゲーション(PC表示時のみ) -->
        <nav class="header-nav">
          <ul>
            <!-- data-mega-target 属性で対応するメガメニューを指定 -->
            <li><button type="button" data-mega-target="company">会社情報</button></li>
            <li><button type="button" data-mega-target="business">事業内容</button></li>
            <li><button type="button" data-mega-target="recruit">採用情報</button></li>
            <li><button type="button" data-mega-target="news">ニュース</button></li>
          </ul>
        </nav>

        <!-- 右端の「お問い合わせ」ボタン(PCのみ) -->
        <div class="header-contact"><a href="#">お問い合わせ</a></div>
      </header>

      <!-- ============================
        メガメニュー
      ============================ -->
      <div class="mega">
        <!-- 背景オーバーレイ(クリックでメニューを閉じる) -->
        <div class="mega-overlay"></div>

        <!-- メガメニュー本体 -->
        <div class="mega-container">

          <!-- ▼ 各セクション(data-mega-id とナビの data-mega-target を対応させる) -->
          <div class="mega-section" data-mega-id="company">
            <!-- SP表示時のアコーディオン見出し -->
            <div class="mega-header"><button type="button">会社情報<span></span></button></div>
            <!-- 中身(PCは常時表示、SPはアコーディオンで開閉) -->
            <div class="mega-body">
              <div class="mega-inner"><!-- メニュー内容(省略) --></div>
            </div>
          </div>

          <div class="mega-section" data-mega-id="business">
            <div class="mega-header"><button type="button">事業内容<span></span></button></div>
            <div class="mega-body">
              <div class="mega-inner"><!-- メニュー内容(省略) --></div>
            </div>
          </div>

          <div class="mega-section" data-mega-id="recruit">
            <div class="mega-header"><button type="button">採用情報<span></span></button></div>
            <div class="mega-body">
              <div class="mega-inner"><!-- メニュー内容(省略) --></div>
            </div>
          </div>

          <div class="mega-section" data-mega-id="news">
            <div class="mega-header"><button type="button">ニュース<span></span></button></div>
            <div class="mega-body">
              <div class="mega-inner"><!-- メニュー内容(省略) --></div>
            </div>
          </div>
          <!-- ▲ 各セクションここまで -->

        </div>
      </div>
      <div class="keyvisual"><!-- キービジュアル(省略) --></div>
      <div class="contents"><!-- コンテンツ(省略) --></div>
      <footer class="footer"><!-- フッター(省略) --></footer>
    </div>
  </body>
</html>

CSSコード

CSSでは、ヘッダーやメガメニューの基本レイアウト、表示・非表示のアニメーション、レスポンシブ対応などを定義しています。特別なテクニックは使わず、メディアクエリとトランジションを組み合わせた、素直な構成です。

/* ==============================
  Header(ヘッダーまわり)
  - 位置・高さはブレークポイントで切替
  - PC: 120px / SP: 80px
============================== */
.header {
  position: relative; /* 子要素の絶対配置の基準にする */
  z-index: 200; /* メガメニューより上(オーバーレイz=100) */
  background: #fff;
}

@media (min-width: 768px) {
  .header {
    height: 120px; /* PC高さ */
  }
}

@media (max-width: 767px) {
  .header {
    height: 80px; /* SP高さ */
  }
}

/* ロゴ:縦中央配置。左右位置・サイズはレスポンシブで調整 */
.header-logo {
  position: absolute;
  top: 50%;
  font-weight: 900;
  line-height: 1;
  transform: translateY(-50%);
}

@media (min-width: 768px) {
  .header-logo {
    left: 50px;
    font-size: 40px;
    letter-spacing: 2px;
  }
}

@media (max-width: 767px) {
  .header-logo {
    left: 20px;
    font-size: 25px;
    letter-spacing: 1px;
  }
}

.header-logo a {
  display: inline-block;
  color: #192753;
  text-decoration: none;
  vertical-align: top;
}

/* ハンバーガー:SPのみ表示。アイコンはspanで描画しopen時に変形 */
.header-hamburger {
  position: absolute;
  right: 8.5px;
  top: 50%;
  width: 45px;
  height: 45px;
  transform: translateY(-50%);
}

@media (min-width: 768px) {
  .header-hamburger {
    display: none;
  }
}

.header-hamburger button {
  display: block;
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: none;
  background: none;
  -webkit-appearance: none;
          appearance: none;
  cursor: pointer;
}

/* 3本ライン */
.header-hamburger button > span {
  display: block;
  position: absolute;
  left: 11.5px;
  top: 22px;
  width: 22px;
  height: 1px;
  background: #4466ce;
}

/* 上・中・下のバーに変形トランジションを付与 */
.header-hamburger button > span:nth-child(1) {
  transform: translateY(-6px);
  transition: transform 350ms cubic-bezier(.215, .61, .355, 1);
}

.header-hamburger button > span:nth-child(2) {
  transition: opacity 350ms cubic-bezier(.215, .61, .355, 1);
}

.header-hamburger button > span:nth-child(3) {
  transform: translateY(6px);
  transition: transform 350ms cubic-bezier(.215, .61, .355, 1);
}

/* 開いた状態(×アイコン化) */
.header-hamburger button.is-open > span:nth-child(1) {
  transform: rotate(45deg) translateY(0px);
}

.header-hamburger button.is-open > span:nth-child(2) {
  opacity: 0;
}

.header-hamburger button.is-open > span:nth-child(3) {
  transform: rotate(-45deg) translateY(0px);
}

/* グローバルナビ:PCのみ表示。縦中央に配置 */
.header-nav {
  position: absolute;
  right: 225px;
  top: 50%;
  transform: translateY(-50%);
}

@media (max-width: 767px) {
  .header-nav {
    display: none;
  }
}

.header-nav ul {
  list-style: none;
  display: flex;
}

/* ナビ項目:下線アニメ(:before)でアクティブを演出 */
.header-nav ul li button {
  display: block;
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
  box-sizing: border-box;
  padding: 25px;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  font-weight: 700;
  line-height: 1.4;
  text-decoration: none;
  cursor: pointer;
  -webkit-appearance: none;
          appearance: none;
  transition: color 300ms;
}

.header-nav ul li button::before {
  content: '';
  display: block;
  position: absolute;
  left: 25px;
  bottom: 8px;
  width: calc(100% - 50px);
  height: 1px;
  background: #4466ce;
  transform: scaleX(0);
  transition: transform 350ms cubic-bezier(.215, .61, .355, 1);
}

.header-nav ul li button:hover::before,
.header-nav ul li button:active::before,
.header-nav ul li button.is-active::before {
  transform: scaleX(1);
}

/* 右端の「お問い合わせ」:PCのみ */
.header-contact {
  position: absolute;
  right: 50px;
  top: 50%;
  transform: translateY(-50%);
}

@media (max-width: 767px) {
  .header-contact {
    display: none;
  }
}

.header-contact a {
  display: block;
  width: 150px;
  padding: 10px 0;
  border-radius: 5px;
  background: #4466ce;
  color: #fff;
  font-weight: 700;
  line-height: 1.4;
  text-align: center;
  text-decoration: none;
  transition: background-color 300ms;
}

.header-contact a:hover {
  background: #192753;
}

/* ==============================
  Mega Menu(メガメニュー)
  - .mega.is-open でオーバーレイ/本体を表示
  - PC: ヘッダー直下に重ねる
  - SP: パディングを広めに
============================== */

/* 背景オーバーレイ(ぼかし+半透明)。
   非表示時は visibility/opacity/pointer-events で無効化 */
.mega-overlay {
  position: fixed;
  left: 0;
  top: 0;
  z-index: 100;
  width: 100%;
  height: 100%;
  background: rgba(0, 0, 0, .5);
  -webkit-backdrop-filter: blur(5px);
          backdrop-filter: blur(5px);
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
  transition: visibility 0ms 400ms, opacity 400ms linear;
}

/* メガメニュー本体コンテナ。高さアニメはJSで付与 */
.mega-container {
  position: absolute;
  left: 0;
  z-index: 100;
  width: 100%;
  box-sizing: border-box;
  background: #fff;
  overflow: hidden;
  visibility: hidden;
  opacity: 0;
  pointer-events: none;
  transition: visibility 0ms 400ms, opacity 400ms linear;
}

/* PCはヘッダー高に合わせる */
@media (min-width: 768px) {
  .mega-container {
    top: 120px;
  }
}

@media (max-width: 767px) {
  .mega-container {
    top: 80px;
    padding: 0 20px 40px;
  }
}

/* セクション(各パネル)
   PCでは重ねて配置し、表示中だけ is-open で見せる */
@media (min-width: 768px) {
  .mega-section {
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    visibility: hidden;
    opacity: 0;
    transition: visibility 0ms 400ms, opacity 400ms linear;
  }
}

/* SPではアコーディオンの見切れ線 */
@media (max-width: 767px) {
  .mega-section {
    border-bottom: 1px solid #e1e1e1;
  }
}

/* 見出し(SPのみ表示、アコーディオンのトリガー) */
.mega-header {
  font-size: 16px;
  font-weight: 700;
  line-height: 1.5;
}

@media (min-width: 768px) {
  .mega-header {
    display: none;
  }
}

.mega-header button {
  display: flex;
  align-items: center;
  position: relative;
  width: 100%;
  height: 65px;
  margin: 0;
  padding: 0 20px;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  -webkit-appearance: none;
          appearance: none;
  cursor: pointer;
}

/* 見出し右の+(span疑似要素で線を描画) */
.mega-header button > span {
  display: block;
  position: absolute;
  right: 20px;
  top: 50%;
  width: 15px;
  height: 15px;
  margin-top: -7.5px;
}

.mega-header button > span::before,
.mega-header button > span::after {
  content: "";
  display: block;
  position: absolute;
  left: 0;
  top: 7px;
  width: 15px;
  height: 1px;
  background: #333;
}

.mega-header button > span::after {
  transform: rotate(90deg);
  transition: transform 350ms cubic-bezier(.215, .61, .355, 1);
}

/* 本文領域(PCは常に表示、SPは閉じておく) */
.mega-body {
  position: relative;
}

@media (min-width: 768px) {
  .mega-body {
    display: block !important;
  }
}

@media (max-width: 767px) {
  .mega-body {
    display: none;
    overflow: hidden;
  }
}

/* 内側余白:PCは左右パディング広め、SPは下マージンのみ */
@media (min-width: 768px) {
  .mega-inner {
    padding: 20px 50px 40px;
  }
}

@media (max-width: 767px) {
  .mega-inner {
    padding-bottom: 25px;
  }
}

/* SP: 先頭セクションの上線(見切れ調整) */
@media (max-width: 767px) {
  .mega-section:first-child {
    border-top: 1px solid #e1e1e1;
  }
}

/* 表示中のセクション。PCで前面に出し、フェードを有効化 */
.mega-section.is-open {
  position: relative;
  left: auto;
  top: auto;
  width: auto;
  visibility: visible;
  opacity: 1;
  transition: visibility 0ms, opacity 400ms linear;
}

/* アコーディオン展開時は+を−に変形 */
.mega-section.is-expanded .mega-header button > span::after {
  transform: rotate(0deg);
}

/* メガメニュー表示状態:オーバーレイ/本体の表示をまとめてON */
.mega.is-open .mega-overlay,
.mega.is-open .mega-container {
  visibility: visible;
  opacity: 1;
  pointer-events: auto;
  transition: visibility 0ms, opacity 400ms linear;
}

/* ==============================
  その他の装飾は本題外のため省略
============================== */

JavaScriptコード

JavaScriptでは、メニューの開閉やパネル切り替え、スマホ時のアコーディオン展開など、表示の制御を行っています。ライブラリは使わず、バニラJSで実装しているため、どこで何をしているのかが把握しやすいよう、シンプルな構成にしています。

document.addEventListener('DOMContentLoaded', () => {
  // ヘッダーが存在しないページでは処理をしない
  if (!document.querySelector('.header')) return;

  // ------------------------------
  // 要素の取得
  // ------------------------------
  const els = {
    hamburgerBtn: document.querySelector('.header-hamburger button'), // ハンバーガーボタン
    headerNav: document.querySelector('.header-nav'), // グローバルナビ
    mega: document.querySelector('.mega'), // メガメニュー全体
    megaOverlay: document.querySelector('.mega-overlay'), // 背景のオーバーレイ
    megaContainer: document.querySelector('.mega-container'), // メガメニュー内のラッパー
  };

  // ------------------------------
  // アニメーションの設定値
  // ------------------------------
  const props = {
    megaFadeDuration: 400, // メガメニューの開閉フェード時間
    megaSwitchDuration: 400, // メガメニュー切り替え時の高さアニメーション時間
    megaSwitchEasing: 'cubic-bezier(0.215, 0.61, 0.355, 1)', // 切り替え時のイージング
    hamburgerAccordionDuration: 400, // ハンバーガーメニューのアコーディオン開閉時間
    hamburgerAccordionEasing: 'cubic-bezier(0.215, 0.61, 0.355, 1)', // アコーディオンのイージング
  };

  // ------------------------------
  // 初期化処理(イベント登録)
  // ------------------------------
  function init() {
    // PC: ナビゲーションのクリック
    els.headerNav.addEventListener('click', (e) => {
      const targetBtn = e.target.closest('button[data-mega-target]');
      if (!targetBtn) return;

      e.stopPropagation();

      // アニメーション中は処理しない
      if (!els.mega.classList.contains('is-animating')) {
        const id = targetBtn.getAttribute('data-mega-target');

        if (targetBtn.classList.contains('is-active')) {
          // すでに開いているメニューのボタンを押した → メガメニューを閉じる
          closeMega();
        } else if (els.mega.classList.contains('is-open')) {
          // 他のメニューが開いている状態で別のボタンを押した → 表示を切り替える
          switchMega(id);
        } else {
          // まだメガメニューが閉じている状態でボタンを押した → 新しく開く
          openMega(id);
        }
      }
    });

    // SP: ハンバーガーボタンのクリック
    els.hamburgerBtn.addEventListener('click', (e) => {
      e.stopPropagation();

      if (!els.mega.classList.contains('is-animating')) {
        if (!els.mega.classList.contains('is-open')) {
          // 最初のパネル(1つ目のdata-mega-target)を開く
          // → 引数なしでエラーにならないように&PC表示に切り替えたときに空表示を防ぐため
          const firstId = els.headerNav.querySelector('button[data-mega-target]').getAttribute('data-mega-target');
          openMega(firstId);
        } else {
          closeMega();
        }
      }
    });

    // オーバーレイのクリックで閉じる
    els.megaOverlay.addEventListener('click', (e) => {
      e.stopPropagation();

      if (!els.mega.classList.contains('is-animating')) {
        closeMega();
      }
    });

    // SP: アコーディオンの開閉
    els.megaContainer.addEventListener('click', (e) => {
      const accBtn = e.target.closest('.mega-header button');
      if (!accBtn) return;

      e.stopPropagation();

      const section = accBtn.closest('.mega-section');

      if (!section.classList.contains('is-animating')) {
        if (!section.classList.contains('is-expanded')) {
          openHamburgerAccordion(section);
        } else {
          closeHamburgerAccordion(section);
        }
      }
    });
  }

  // ------------------------------
  // メガメニューを開く
  // ------------------------------
  function openMega(id) {
    els.mega.classList.add('is-animating', 'is-open');
    els.hamburgerBtn.classList.add('is-open');
    els.headerNav.querySelector(`button[data-mega-target="${id}"]`).classList.add('is-active');
    els.mega.querySelector(`.mega-section[data-mega-id="${id}"]`).classList.add('is-open');

    setTimeout(() => {
      els.mega.classList.remove('is-animating');
    }, props.megaFadeDuration);
  }

  // ------------------------------
  // メガメニューのパネルを切り替える
  // ------------------------------
  function switchMega(id) {
    els.mega.classList.add('is-animating');

    // 切り替え前後の高さを取得
    const startHeight = els.megaContainer.offsetHeight;
    els.mega.querySelector('.mega-section.is-open').classList.remove('is-open');
    els.mega.querySelector(`.mega-section[data-mega-id="${id}"]`).classList.add('is-open');
    const endHeight = els.megaContainer.offsetHeight;

    // ナビのアクティブ状態を更新
    els.headerNav.querySelector('button.is-active').classList.remove('is-active');
    els.headerNav.querySelector(`button[data-mega-target="${id}"]`).classList.add('is-active');

    // 高さアニメーション
    els.megaContainer.style.height = `${startHeight}px`;
    els.megaContainer.animate([
      { height: `${startHeight}px` },
      { height: `${endHeight}px` }
    ], {
      duration: props.megaSwitchDuration,
      easing: props.megaSwitchEasing
    }).onfinish = () => {
      els.megaContainer.style.height = 'auto';
      els.mega.classList.remove('is-animating');
    };
  }

  // ------------------------------
  // メガメニューを閉じる
  // ------------------------------
  function closeMega() {
    els.mega.classList.add('is-animating');
    els.mega.classList.remove('is-open');
    els.hamburgerBtn.classList.remove('is-open');
    els.headerNav.querySelector('button.is-active').classList.remove('is-active');

    setTimeout(() => {
      els.mega.querySelector('.mega-section.is-open').classList.remove('is-open');
      els.mega.classList.remove('is-animating');
    }, props.megaFadeDuration);
  }

  // ------------------------------
  // SP: アコーディオンを開く
  // ------------------------------
  function openHamburgerAccordion(section) {
    section.classList.add('is-animating', 'is-expanded');
    const body = section.querySelector('.mega-body');
    body.style.display = 'block';
    const endHeight = body.offsetHeight;
    body.style.height = '0px';

    body.animate([
      { height: '0px' },
      { height: `${endHeight}px` }
    ], {
      duration: props.hamburgerAccordionDuration,
      easing: props.hamburgerAccordionEasing
    }).onfinish = () => {
      body.style.height = 'auto';
      section.classList.remove('is-animating');
    };
  }

  // ------------------------------
  // SP: アコーディオンを閉じる
  // ------------------------------
  function closeHamburgerAccordion(section) {
    section.classList.add('is-animating');
    section.classList.remove('is-expanded');
    const body = section.querySelector('.mega-body');
    const startHeight = body.offsetHeight;

    body.animate([
      { height: `${startHeight}px` },
      { height: '0px' }
    ], {
      duration: props.hamburgerAccordionDuration,
      easing: props.hamburgerAccordionEasing
    }).onfinish = () => {
      body.style.display = 'none';
      body.style.height = 'auto';
      section.classList.remove('is-animating');
    };
  }

  // 初期化実行
  init();
});

コードのポイント解説

ここまで掲載したコードは少しボリュームがありますが、実装の考え方そのものはシンプルです。

このセクションでは、特に重要なポイントをいくつかピックアップして解説していきます。どの部分で何をしているのかを理解しておくと、実務でも応用しやすくなりますよ

ポイント1:クリックイベントで状態を整理して分岐

// PC: ナビゲーションのクリック
els.headerNav.addEventListener('click', (e) => {
  const targetBtn = e.target.closest('button[data-mega-target]');
  if (!targetBtn) return;

  e.stopPropagation();

  // アニメーション中は処理しない
  if (!els.mega.classList.contains('is-animating')) {
    const id = targetBtn.getAttribute('data-mega-target');

    if (targetBtn.classList.contains('is-active')) {
      // すでに開いているメニューのボタンを押した → メガメニューを閉じる
      closeMega();
    } else if (els.mega.classList.contains('is-open')) {
      // 他のメニューが開いている状態で別のボタンを押した → 表示を切り替える
      switchMega(id);
    } else {
      // まだメガメニューが閉じている状態でボタンを押した → 新しく開く
      openMega(id);
    }
  }
});

メガメニューの開閉は、クリックされたボタンの状態と、現在のメガメニューの状態を見て3パターンに分岐しています

  • すでに開いているボタン → closeMega()
  • メガメニューが開いていて別のボタンが押された → switchMega(id)
  • メガメニューが閉じている → openMega(id)

この分岐をちゃんと整理しておくことで、ボタンを連打したときに予想外の動きをする…といった不具合を防げます

ポイント2:開閉状態はクラスで一元管理

// ------------------------------
// メガメニューを開く
// ------------------------------
function openMega(id) {
  els.mega.classList.add('is-animating', 'is-open');
  els.hamburgerBtn.classList.add('is-open');
  els.headerNav.querySelector(`button[data-mega-target="${id}"]`).classList.add('is-active');
  els.mega.querySelector(`.mega-section[data-mega-id="${id}"]`).classList.add('is-open');

  setTimeout(() => {
    els.mega.classList.remove('is-animating');
  }, props.megaFadeDuration);
}

is-openis-animating などの状態管理クラスを軸に、開閉をコントロールしています。直接 style.display を書き換えるのではなくクラスで一元管理することで、CSSとロジックを分離し、読みやすく保守しやすいコードになっています。

みなと
みなと

動きの実装は、できるだけCSSに寄せておくと保守がラクになります。JavaScriptはあくまで状態の切り替えに集中させる、という分担がポイントです。

ポイント3:高さをアニメーションして自然な切り替え

// ------------------------------
// メガメニューのパネルを切り替える
// ------------------------------
function switchMega(id) {
  els.mega.classList.add('is-animating');

  // 切り替え前後の高さを取得
  const startHeight = els.megaContainer.offsetHeight;
  els.mega.querySelector('.mega-section.is-open').classList.remove('is-open');
  els.mega.querySelector(`.mega-section[data-mega-id="${id}"]`).classList.add('is-open');
  const endHeight = els.megaContainer.offsetHeight;

  // ナビのアクティブ状態を更新
  els.headerNav.querySelector('button.is-active').classList.remove('is-active');
  els.headerNav.querySelector(`button[data-mega-target="${id}"]`).classList.add('is-active');

  // 高さアニメーション
  els.megaContainer.style.height = `${startHeight}px`;
  els.megaContainer.animate([
    { height: `${startHeight}px` },
    { height: `${endHeight}px` }
  ], {
    duration: props.megaSwitchDuration,
    easing: props.megaSwitchEasing
  }).onfinish = () => {
    els.megaContainer.style.height = 'auto';
    els.mega.classList.remove('is-animating');
  };
}

メガメニューを切り替えるときは、高さをアニメーションさせて、切り替えがなめらかに見えるようにしています。高さが可変の場合、CSSだけでスムーズなアニメーションを行うのは難しいため、ここでは JavaScript を使って実現しています。

具体的には、element.animate() を使って高さの変化をなめらかに表現しています。この animate()Web Animations API のメソッドで、jQuery の .animate() に頼らなくても、シンプルでモダンなアニメーションが実装できるのが特徴です

みなと
みなと

最近は“とりあえずjQuery”ではなく、標準APIで完結させることが増えました。軽量に仕上がるので、実務でもかなり重宝してます。

ポイント4:スマホではアコーディオンをアニメーション

// ------------------------------
// SP: アコーディオンを開く
// ------------------------------
function openHamburgerAccordion(section) {
  section.classList.add('is-animating', 'is-expanded');
  const body = section.querySelector('.mega-body');
  body.style.display = 'block';
  const endHeight = body.offsetHeight;
  body.style.height = '0px';

  body.animate([
    { height: '0px' },
    { height: `${endHeight}px` }
  ], {
    duration: props.hamburgerAccordionDuration,
    easing: props.hamburgerAccordionEasing
  }).onfinish = () => {
    body.style.height = 'auto';
    section.classList.remove('is-animating');
  };
}

スマホ表示では、メガメニューをアコーディオン形式で開閉できるようにしています。メニュー項目をタップすると、そのセクションの中身がスライドするように展開・収納される仕組みです。ここでも、高さが可変な要素をアニメーションさせる必要があるため、CSS ではなくJavaScript を使って開閉アニメーションを制御しています。

閉じた状態(display: none)では高さを取得できないため、少し工夫が必要です

  1. 一瞬だけ display: block にして、要素を表示可能な状態にする。
  2. 実際の高さを取得する(scrollHeight などを利用)。
  3. 取得後、いったん高さを 0px に戻して閉じた状態を作る。
  4. element.animate()0 → 実際の高さにアニメーションする。
スワン
スワン

この手法は「高さが事前にわからない(=可変)」要素に対して非常によく使われる定番のテクニックです。

まとめ

今回紹介したのは、PCではメガメニュー、スマホではハンバーガー+アコーディオンという、実務でもよく使われる基本パターンの実装例でした。複雑な装飾やライブラリを使わなくても、HTML・CSS・JavaScript を組み合わせることで、十分実用的なメガメニューを作ることができます。

メガメニューはサイトによってデザインや動きが本当にさまざまなので、まずは今回のようなシンプルな仕組みをベースにしておくと、あとから細かい仕様変更や拡張にも柔軟に対応できます

ぜひ今回のコードを参考に、自分のプロジェクトに合わせてアレンジしてみてください!

押していただけると励みになります!

ABOUT ME
みなと
みなと
フロントエンドエンジニア
東京のWeb制作会社で15年以上働いている現役フロントエンドエンジニアです。これまで、いろんなプロジェクトに関わりながら、フロントエンド開発やWebデザインに取り組んできました。このブログでは、今までの経験を活かして、Web制作に役立つ情報やノウハウをシェアしていきたいと思います。初心者の方から、現場で働く方まで、誰でも参考にできる内容をお届けしますので、ぜひ覗いてみてください。
記事URLをコピーしました