CSSでハンバーガーメニューを実装する方法|スマートフォン対応ナビゲーションの開閉・アニメーション・オーバーレイ


はじめに

前回の記事ではJavaScriptでスクロールアニメーションを実装する方法としてIntersectionObserverを使ったフェードイン・stagger・カウントアップを解説しました。今回はCSSとJavaScriptでハンバーガーメニューを実装する方法を解説します。スマートフォン対応のナビゲーションは副業案件でほぼ必ず登場します。「ハンバーガーアイコンをクリックするとメニューが開く」というシンプルな機能ですが、アニメーションやオーバーレイまで含めて丁寧に解説します。


1. ハンバーガーメニューの基本構成

1.1 全体の仕組みを理解する

ハンバーガーメニューの構成要素:

① ハンバーガーボタン
   → 3本線のアイコン
   → クリックで開閉する
   → スマートフォンのみ表示

② ナビゲーションメニュー
   → 通常は非表示
   → ボタンをクリックすると表示

③ オーバーレイ(背景の暗いやつ)
   → メニューが開いているとき背景を暗くする
   → クリックするとメニューが閉じる

④ body のスクロール固定
   → メニューが開いているとき背景をスクロールさせない

動作の流れ:
ボタンをクリック
→ ボタン・メニュー・オーバーレイに
  is-open クラスを付ける/外す
→ CSSでアニメーション

1.2 HTMLの構造

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>サイトタイトル</title>
    <link rel="stylesheet" href="css/style.css">
</head>
<body>

    <header class="header">
        <div class="header-inner">

            <!-- ロゴ -->
            <a class="header-logo" href="/">
                <img src="images/logo.svg" alt="ロゴ" width="120" height="40">
            </a>

            <!-- PC用ナビゲーション(常に表示) -->
            <nav class="header-nav" id="nav" aria-label="メインナビゲーション">
                <ul class="nav-list">
                    <li class="nav-item">
                        <a class="nav-link" href="/">ホーム</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/about/">会社概要</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/service/">サービス</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/works/">実績</a>
                    </li>
                    <li class="nav-item">
                        <a class="nav-link" href="/contact/">お問い合わせ</a>
                    </li>
                </ul>
            </nav>

            <!-- ハンバーガーボタン(スマートフォンのみ表示) -->
            <button
                class="hamburger"
                id="hamburger"
                aria-label="メニューを開く"
                aria-expanded="false"
                aria-controls="nav"
            >
                <span class="hamburger-line"></span>
                <span class="hamburger-line"></span>
                <span class="hamburger-line"></span>
            </button>

        </div>
    </header>

    <!-- オーバーレイ -->
    <div class="overlay" id="overlay"></div>

    <main>
        <!-- ページのコンテンツ -->
    </main>

    <script src="js/main.js"></script>
</body>
</html>

2. CSSの実装

2.1 ヘッダーの基本スタイル

/* ===== リセット・ベース ===== */
*,
*::before,
*::after {
    box-sizing: border-box;
    margin:     0;
    padding:    0;
}

/* ===== ヘッダー ===== */
.header {
    position:   fixed; /* 画面上部に固定 */
    top:        0;
    left:       0;
    right:      0;
    z-index:    100;
    background: #fff;
    box-shadow: 0 2px 12px rgba( 0, 0, 0, 0.08 );
}

.header-inner {
    display:         flex;
    align-items:     center;
    justify-content: space-between;
    max-width:       1200px;
    margin:          0 auto;
    padding:         0 24px;
    height:          70px;
}

/* ロゴ */
.header-logo img {
    display: block;
    height:  40px;
    width:   auto;
}

/* ===== PC用ナビゲーション ===== */
.nav-list {
    display:    flex;
    gap:        32px;
    list-style: none;
}

.nav-link {
    color:           #333;
    text-decoration: none;
    font-size:       1.5rem;
    font-weight:     500;
    transition:      color 0.2s ease;
}

.nav-link:hover {
    color: #1A5FC0;
}

/* ===== ハンバーガーボタン(PCでは非表示) ===== */
.hamburger {
    display:       none; /* PCでは非表示 */
    flex-direction: column;
    justify-content: center;
    gap:            5px;
    width:          44px;
    height:         44px;
    background:     none;
    border:         none;
    cursor:         pointer;
    padding:        8px;
    border-radius:  6px;
    transition:     background 0.2s ease;
}

.hamburger:hover {
    background: #f0f0f0;
}

/* 3本線 */
.hamburger-line {
    display:       block;
    width:         100%;
    height:        2px;
    background:    #333;
    border-radius: 2px;
    transition:    transform  0.3s ease,
                   opacity    0.3s ease;
    transform-origin: center;
}

2.2 スマートフォン用のスタイル

/* ===== スマートフォン用 ===== */
@media ( max-width: 768px ) {

    /* ハンバーガーボタンを表示する */
    .hamburger {
        display: flex;
    }

    /* ナビゲーションをスマートフォン用に変更する */
    .header-nav {
        position:   fixed;
        top:        70px; /* ヘッダーの高さ分下げる */
        right:      0;
        bottom:     0;
        width:      min( 300px, 85vw ); /* 最大300px・画面幅の85% */
        background: #fff;
        padding:    32px 24px;
        transform:  translateX( 100% ); /* 右側に隠す */
        transition: transform 0.35s cubic-bezier( 0.4, 0, 0.2, 1 );
        overflow-y: auto;
        z-index:    200;
        box-shadow: -4px 0 24px rgba( 0, 0, 0, 0.12 );
    }

    /* メニューが開いているとき */
    .header-nav.is-open {
        transform: translateX( 0 );
    }

    /* ナビリストを縦並びにする */
    .nav-list {
        flex-direction: column;
        gap:            0;
    }

    .nav-item {
        border-bottom: 1px solid #f0f0f0;
    }

    .nav-item:first-child {
        border-top: 1px solid #f0f0f0;
    }

    .nav-link {
        display:      block;
        padding:      16px 8px;
        font-size:    1.6rem;
    }
}

2.3 ハンバーガーアイコンのアニメーション

/*
    ボタンに is-open クラスが付いたとき
    3本線を × に変形させる
*/

/* 1本目:右斜め上に回転 */
.hamburger.is-open .hamburger-line:nth-child(1) {
    transform: translateY( 7px ) rotate( 45deg );
}

/* 2本目:消える */
.hamburger.is-open .hamburger-line:nth-child(2) {
    opacity:   0;
    transform: scaleX( 0 );
}

/* 3本目:右斜め下に回転 */
.hamburger.is-open .hamburger-line:nth-child(3) {
    transform: translateY( -7px ) rotate( -45deg );
}

/*
    gap: 5px で3本線が並んでいるとき
    1本目と3本目を真ん中に向かって移動させると
    きれいに × になる

    計算式:
    gap の合計 = 5px × 2 = 10px
    line の height = 2px × 2 = 4px
    移動量 = ( 10px + 4px ) / 2 = 7px
*/

2.4 オーバーレイのスタイル

/* ===== オーバーレイ ===== */
.overlay {
    position:       fixed;
    inset:          0;
    background:     rgba( 0, 0, 0, 0.5 );
    z-index:        150; /* ナビより下・ヘッダーより上 */
    opacity:        0;
    pointer-events: none; /* クリックを透過させる */
    transition:     opacity 0.35s ease;
}

/* オーバーレイが表示されているとき */
.overlay.is-open {
    opacity:        1;
    pointer-events: auto; /* クリックを有効にする */
}

/*
    z-index の整理:
    ヘッダー   : z-index 100
    オーバーレイ: z-index 150
    ナビゲーション: z-index 200

    → オーバーレイがヘッダーとナビの間に入る
    → ナビはオーバーレイの上に表示される
*/

2.5 bodyのスクロール固定

/*
    メニューが開いているとき
    背景のスクロールを止める
*/
body.is-nav-open {
    overflow: hidden;
}

3. JavaScriptの実装

3.1 シンプルな実装

// 要素を取得する
const hamburger = document.getElementById( 'hamburger' );
const nav       = document.getElementById( 'nav' );
const overlay   = document.getElementById( 'overlay' );

// メニューを開く関数
function openMenu() {
    hamburger.classList.add( 'is-open' );
    nav.classList.add( 'is-open' );
    overlay.classList.add( 'is-open' );
    document.body.classList.add( 'is-nav-open' );

    // アクセシビリティ:aria属性を更新する
    hamburger.setAttribute( 'aria-expanded', 'true' );
    hamburger.setAttribute( 'aria-label', 'メニューを閉じる' );
}

// メニューを閉じる関数
function closeMenu() {
    hamburger.classList.remove( 'is-open' );
    nav.classList.remove( 'is-open' );
    overlay.classList.remove( 'is-open' );
    document.body.classList.remove( 'is-nav-open' );

    // アクセシビリティ:aria属性を更新する
    hamburger.setAttribute( 'aria-expanded', 'false' );
    hamburger.setAttribute( 'aria-label', 'メニューを開く' );
}

// ハンバーガーボタンのクリック
hamburger.addEventListener( 'click', () => {
    if ( hamburger.classList.contains( 'is-open' ) ) {
        closeMenu();
    } else {
        openMenu();
    }
} );

// オーバーレイのクリックで閉じる
overlay.addEventListener( 'click', closeMenu );

// Escキーで閉じる
document.addEventListener( 'keydown', ( e ) => {
    if ( e.key === 'Escape' ) closeMenu();
} );

// ナビのリンクをクリックしたら閉じる
nav.querySelectorAll( '.nav-link' ).forEach( ( link ) => {
    link.addEventListener( 'click', closeMenu );
} );

// 画面幅が広くなったら閉じる
window.addEventListener( 'resize', () => {
    if ( window.innerWidth > 768 ) closeMenu();
} );

3.2 クラスにまとめた実装

/*
    実案件で使いやすいようにクラスにまとめた実装。
    複数ページで使い回しやすくなる。
*/

class HamburgerMenu {
    #isOpen = false;

    constructor() {
        this.hamburger = document.getElementById( 'hamburger' );
        this.nav       = document.getElementById( 'nav' );
        this.overlay   = document.getElementById( 'overlay' );

        if ( !this.hamburger || !this.nav || !this.overlay ) return;

        this.#bindEvents();
    }

    // メニューを開く
    open() {
        this.#isOpen = true;
        this.hamburger.classList.add( 'is-open' );
        this.nav.classList.add( 'is-open' );
        this.overlay.classList.add( 'is-open' );
        document.body.classList.add( 'is-nav-open' );
        this.hamburger.setAttribute( 'aria-expanded', 'true' );
        this.hamburger.setAttribute( 'aria-label', 'メニューを閉じる' );
    }

    // メニューを閉じる
    close() {
        this.#isOpen = false;
        this.hamburger.classList.remove( 'is-open' );
        this.nav.classList.remove( 'is-open' );
        this.overlay.classList.remove( 'is-open' );
        document.body.classList.remove( 'is-nav-open' );
        this.hamburger.setAttribute( 'aria-expanded', 'false' );
        this.hamburger.setAttribute( 'aria-label', 'メニューを開く' );
    }

    // 開閉を切り替える
    toggle() {
        this.#isOpen ? this.close() : this.open();
    }

    #bindEvents() {
        // ハンバーガーボタン
        this.hamburger.addEventListener( 'click', () => this.toggle() );

        // オーバーレイ
        this.overlay.addEventListener( 'click', () => this.close() );

        // Escキー
        document.addEventListener( 'keydown', ( e ) => {
            if ( e.key === 'Escape' && this.#isOpen ) this.close();
        } );

        // ナビリンク
        this.nav.querySelectorAll( '.nav-link' ).forEach( ( link ) => {
            link.addEventListener( 'click', () => this.close() );
        } );

        // リサイズ
        window.addEventListener( 'resize', () => {
            if ( window.innerWidth > 768 ) this.close();
        } );
    }

    // 現在の状態を取得する
    get isOpen() {
        return this.#isOpen;
    }
}

// 初期化
document.addEventListener( 'DOMContentLoaded', () => {
    new HamburgerMenu();
} );

4. アニメーションのバリエーション

css

4.1 上からスライドイン

/* 右からスライドのかわりに上からスライドする */
@media ( max-width: 768px ) {
    .header-nav {
        position:   fixed;
        top:        70px;
        left:       0;
        right:      0;
        background: #fff;
        padding:    24px;
        transform:  translateY( -100% ); /* 上に隠す */
        transition: transform 0.35s cubic-bezier( 0.4, 0, 0.2, 1 );
        z-index:    200;
        box-shadow: 0 8px 24px rgba( 0, 0, 0, 0.12 );
    }

    .header-nav.is-open {
        transform: translateY( 0 );
    }
}

4.2 フルスクリーンメニュー

/* 画面全体を覆うフルスクリーンメニュー */
@media ( max-width: 768px ) {
    .header-nav {
        position:        fixed;
        inset:           0;
        background:      #111;
        display:         flex;
        align-items:     center;
        justify-content: center;
        opacity:         0;
        pointer-events:  none;
        transition:      opacity 0.4s ease;
        z-index:         200;
    }

    .header-nav.is-open {
        opacity:        1;
        pointer-events: auto;
    }

    .nav-list {
        flex-direction: column;
        align-items:    center;
        gap:            0;
    }

    .nav-link {
        color:       #fff;
        font-size:   2.4rem;
        font-weight: 700;
        padding:     16px 32px;
    }

    .nav-link:hover {
        color: #F7DF1E;
    }
}

4.3 ナビアイテムを順番に表示する

/*
    メニューが開いたとき
    ナビアイテムを順番にフェードインさせる
*/
@media ( max-width: 768px ) {
    .nav-item {
        opacity:   0;
        transform: translateX( 20px );
        transition: opacity   0.3s ease,
                    transform 0.3s ease;
    }

    /* 各アイテムに遅延を付ける */
    .nav-item:nth-child(1) { transition-delay: 0.05s;  }
    .nav-item:nth-child(2) { transition-delay: 0.10s;  }
    .nav-item:nth-child(3) { transition-delay: 0.15s;  }
    .nav-item:nth-child(4) { transition-delay: 0.20s;  }
    .nav-item:nth-child(5) { transition-delay: 0.25s;  }

    /* メニューが開いているとき表示 */
    .header-nav.is-open .nav-item {
        opacity:   1;
        transform: translateX( 0 );
    }

    /* メニューが閉じるとき遅延をリセット */
    .header-nav:not( .is-open ) .nav-item {
        transition-delay: 0s;
    }
}

5. よくある問題と対処法

5.1 iOSでbodyのスクロール固定が効かない

css

/*
    iOSではoverflow: hiddenだけでは
    スクロールが止まらないことがある。
    position: fixedを使う。
*/

/* JavaScriptでスクロール位置を保存してから固定する */
javascript

class HamburgerMenu {
    #scrollY = 0;

    open() {
        // スクロール位置を保存する
        this.#scrollY = window.scrollY;

        // bodyを固定する
        document.body.style.position = 'fixed';
        document.body.style.top      = `-${ this.#scrollY }px`;
        document.body.style.left     = '0';
        document.body.style.right    = '0';

        // クラスを付ける
        this.hamburger.classList.add( 'is-open' );
        this.nav.classList.add( 'is-open' );
        this.overlay.classList.add( 'is-open' );
        this.hamburger.setAttribute( 'aria-expanded', 'true' );
        this.hamburger.setAttribute( 'aria-label', 'メニューを閉じる' );
    }

    close() {
        // bodyの固定を解除して
        // スクロール位置を元に戻す
        document.body.style.position = '';
        document.body.style.top      = '';
        document.body.style.left     = '';
        document.body.style.right    = '';
        window.scrollTo( 0, this.#scrollY );

        // クラスを外す
        this.hamburger.classList.remove( 'is-open' );
        this.nav.classList.remove( 'is-open' );
        this.overlay.classList.remove( 'is-open' );
        this.hamburger.setAttribute( 'aria-expanded', 'false' );
        this.hamburger.setAttribute( 'aria-label', 'メニューを開く' );
    }
}

5.2 PCに戻ったときメニューが残る

javascript

/*
    リサイズイベントで閉じるが
    リサイズのたびに何度も発火するので
    debounce を使って処理を間引く
*/

let resizeTimer;
window.addEventListener( 'resize', () => {
    clearTimeout( resizeTimer );
    resizeTimer = setTimeout( () => {
        if ( window.innerWidth > 768 ) {
            menu.close();
        }
    }, 200 );
} );

6. 実践チェックリスト

✓ ハンバーガーボタンは display: none でPCでは非表示にして・スマートフォン用のメディアクエリで display: flex にしているか

✓ ナビゲーションは transform: translateX( 100% ) で右に隠して・is-open クラスで translateX( 0 ) にスライドインしているか

✓ ハンバーガーアイコンの3本線を transform で × に変形させているか

✓ オーバーレイは pointer-events: none で通常時はクリックを透過させているか

✓ aria-expanded と aria-label をJavaScriptで更新してアクセシビリティに対応しているか

✓ オーバーレイのクリック・Escキー・ナビリンクのクリック・リサイズでメニューが閉じるか

✓ iOSのスクロール固定に position: fixed と scrollY の保存を使っているか

✓ transition に cubic-bezier を使ってアニメーションに自然な動きを付けているか


まとめ

今回はCSSとJavaScriptでハンバーガーメニューを実装する方法を解説しました。ポイントをまとめると:

  • ハンバーガーボタンはPCでは display: none・スマートフォン用メディアクエリで display: flex にする
  • ナビゲーションは transform: translateX( 100% ) で右に隠して・is-open クラスで translateX( 0 ) にスライドインする
  • 3本線の × 変形は1本目と3本目を translateY で中央に寄せてから rotate で回転させる
  • オーバーレイは opacity: 0 と pointer-events: none で非表示にして・is-open クラスで表示する
  • aria-expanded と aria-label をJavaScriptで更新するとスクリーンリーダー対応になる
  • iOSのスクロール固定は overflow: hidden だけでは不十分なため position: fixed と scrollY の保存を組み合わせる
  • ナビアイテムに transition-delay を順番に付けると開いたときの演出がより自然になる
  • オーバーレイのクリック・Escキー・ナビリンクのクリック・リサイズの4つのクローズトリガーを忘れずに実装する

次の記事では、Web制作でよく使うCSS小技をまとめてご紹介します。テキストの省略・画像のobject-fit・aspect-ratio・中央揃えパターン・グラデーション・ボックスシャドウなど、実案件でそのままコピペして使えるスニペット集をお届けします。お楽しみに!

コメント

タイトルとURLをコピーしました