はじめに
前回の記事では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・中央揃えパターン・グラデーション・ボックスシャドウなど、実案件でそのままコピペして使えるスニペット集をお届けします。お楽しみに!
コメント