😎
React ハンバーガーメニュー
個人開発でシンプルにしたい画面が、機能が増えてごちゃごちゃしてきたのでハンバーガーメニューを実装することにしました。

完成イメージ
この記事で作成するハンバーガーメニューの特徴:
- TypeScriptで型安全な実装
- アニメーション付きの開閉動作
- オーバーレイクリックで閉じる
- アクセシビリティ対応
- クリーンで再利用可能なコンポーネント
基本的な実装
1. コンポーネントの構造
まずは、TypeScriptのインターフェースと基本構造から見ていきましょう。
interface HamburgerMenuProps {
onShowWineList: () => void;
onShowAddForm: () => void;
}
const HamburgerMenu: React.FC<HamburgerMenuProps> = ({
onShowWineList,
onShowAddForm
}) => {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="hamburger-menu">
{/* メニューの内容 */}
</div>
);
};
export default HamburgerMenu;
ポイント:
-
Propsをインターフェースで定義し、型安全性を確保 - 親コンポーネントから関数を受け取る設計
-
React.FCで関数コンポーネントの型を明示
2. 開閉状態の管理
useStateフックを使ってメニューの開閉状態を管理します。
const [isOpen, setIsOpen] = useState(false);
const toggleMenu = () => {
setIsOpen(!isOpen);
};
useState の使い方:
-
isOpen: 現在の状態(true = 開いている、false = 閉じている) -
setIsOpen: 状態を更新する関数 -
toggleMenu: クリックで状態を反転させる関数
3. ハンバーガーボタンの実装
3本線のアイコンを実装します。
<button
className={`hamburger-button ${isOpen ? 'open' : ''}`}
onClick={toggleMenu}
aria-label="メニュー"
>
<span className="hamburger-line"></span>
<span className="hamburger-line"></span>
<span className="hamburger-line"></span>
</button>
重要なポイント:
- テンプレートリテラルで動的にクラスを切り替え
-
aria-labelでアクセシビリティに配慮 - 3つの
span要素で3本線を表現
メニュードロップダウンの実装
4. 条件付きレンダリング
メニューが開いている時だけドロップダウンを表示します。
{isOpen && (
<>
<div className="menu-overlay" onClick={() => setIsOpen(false)} />
<div className="menu-dropdown">
{/* メニュー項目 */}
</div>
</>
)}
条件付きレンダリングの仕組み:
-
{isOpen && <Component />}:isOpenがtrueの時だけコンポーネントを描画 -
<>...</>: Fragmentを使って複数要素をグループ化 - オーバーレイとドロップダウンを同時に表示
5. オーバーレイの実装
背景クリックでメニューを閉じる機能を追加します。
<div
className="menu-overlay"
onClick={() => setIsOpen(false)}
/>
UXのポイント:
- 半透明の背景(オーバーレイ)を配置
- クリックするとメニューが閉じる
- ユーザーが直感的に操作できる
6. メニュー項目の実装
const handleMenuItemClick = (action: () => void) => {
action();
setIsOpen(false);
};
// メニュー項目
<button
className="menu-item"
onClick={() => handleMenuItemClick(onShowAddForm)}
>
📝 感想を追加
</button>
<button
className="menu-item"
onClick={() => handleMenuItemClick(onShowWineList)}
>
📋 レビュー一覧
</button>
実装のポイント:
-
handleMenuItemClick: アクションを実行した後、メニューを閉じる - 絵文字でアイコン代わりに視認性を向上
- 各ボタンに異なるアクションを割り当て
完成系
import React, { useState } from 'react';
interface HamburgerMenuProps {
onShowWineList: () => void;
onShowAddForm: () => void;
}
const HamburgerMenu: React.FC<HamburgerMenuProps> = ({
onShowWineList,
onShowAddForm
}) => {
const [isOpen, setIsOpen] = useState(false);
const toggleMenu = () => {
setIsOpen(!isOpen);
};
const handleMenuItemClick = (action: () => void) => {
action();
setIsOpen(false);
};
return (
<div className="hamburger-menu">
<button
className={`hamburger-button ${isOpen ? 'open' : ''}`}
onClick={toggleMenu}
aria-label="メニュー"
>
<span className="hamburger-line"></span>
<span className="hamburger-line"></span>
<span className="hamburger-line"></span>
</button>
{isOpen && (
<>
<div className="menu-overlay" onClick={() => setIsOpen(false)} />
<div className="menu-dropdown">
<button
className="menu-item"
onClick={() => handleMenuItemClick(onShowAddForm)}
>
📝 感想を追加
</button>
<button
className="menu-item"
onClick={() => handleMenuItemClick(onShowWineList)}
>
📋 レビュー一覧
</button>
</div>
</>
)}
</div>
);
};
export default HamburgerMenu;
CSSスタイリング
アニメーション付きのスタイルを実装します。
/* ハンバーガーメニューのコンテナ */
.hamburger-menu {
position: relative;
z-index: 1000;
}
/* ハンバーガーボタン */
.hamburger-button {
display: flex;
flex-direction: column;
justify-content: space-around;
width: 40px;
height: 40px;
background: transparent;
border: none;
cursor: pointer;
padding: 8px;
z-index: 1001;
}
/* 3本線 */
.hamburger-line {
width: 100%;
height: 3px;
background-color: #333;
border-radius: 2px;
transition: all 0.3s ease;
}
/* 開いた時のアニメーション(×マーク) */
.hamburger-button.open .hamburger-line:nth-child(1) {
transform: translateY(10px) rotate(45deg);
}
.hamburger-button.open .hamburger-line:nth-child(2) {
opacity: 0;
}
.hamburger-button.open .hamburger-line:nth-child(3) {
transform: translateY(-10px) rotate(-45deg);
}
/* オーバーレイ */
.menu-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
animation: fadeIn 0.3s ease;
}
/* ドロップダウンメニュー */
.menu-dropdown {
position: absolute;
top: 50px;
right: 0;
background-color: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
min-width: 200px;
overflow: hidden;
z-index: 1001;
animation: slideDown 0.3s ease;
}
/* メニュー項目 */
.menu-item {
width: 100%;
padding: 16px 20px;
background: none;
border: none;
text-align: left;
cursor: pointer;
font-size: 16px;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.menu-item:hover {
background-color: #f5f5f5;
}
.menu-item:not(:last-child) {
border-bottom: 1px solid #e0e0e0;
}
/* アニメーション */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
CSSのポイント:
-
transformで×マークへのアニメーション -
position: fixedでオーバーレイを全画面に配置 -
@keyframesでスムーズなアニメーション -
transitionでホバー時の変化を滑らかに
親コンポーネントでの使用例
import React, { useState } from 'react';
import HamburgerMenu from './HamburgerMenu';
const App: React.FC = () => {
const [view, setView] = useState<'home' | 'list' | 'form'>('home');
const showWineList = () => {
setView('list');
};
const showAddForm = () => {
setView('form');
};
return (
<div className="app">
<header>
<h1>ワインレビュー</h1>
<HamburgerMenu
onShowWineList={showWineList}
onShowAddForm={showAddForm}
/>
</header>
<main>
{view === 'home' && <HomePage />}
{view === 'list' && <WineList />}
{view === 'form' && <AddForm />}
</main>
</div>
);
};
export default App;
応用編:より高度な機能
1. 外部クリックで閉じる(useEffect活用)
import { useEffect, useRef } from 'react';
const HamburgerMenu: React.FC<HamburgerMenuProps> = ({
onShowWineList,
onShowAddForm
}) => {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isOpen]);
return (
<div className="hamburger-menu" ref={menuRef}>
{/* コンポーネント内容 */}
</div>
);
};
2. キーボード操作対応(Escキーで閉じる)
useEffect(() => {
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscKey);
}
return () => {
document.removeEventListener('keydown', handleEscKey);
};
}, [isOpen]);
3. スクロール無効化
メニューが開いている時、背景のスクロールを無効にします。
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);
4. カスタムフック化
再利用性を高めるため、ロジックをカスタムフックに切り出します。
// useHamburgerMenu.ts
import { useState, useEffect, useRef } from 'react';
export const useHamburgerMenu = () => {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
const toggleMenu = () => {
setIsOpen(!isOpen);
};
const closeMenu = () => {
setIsOpen(false);
};
// 外部クリックで閉じる
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
document.body.style.overflow = 'hidden';
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.body.style.overflow = 'unset';
};
}, [isOpen]);
// Escキーで閉じる
useEffect(() => {
const handleEscKey = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('keydown', handleEscKey);
}
return () => {
document.removeEventListener('keydown', handleEscKey);
};
}, [isOpen]);
return { isOpen, toggleMenu, closeMenu, menuRef };
};
使用例:
const HamburgerMenu: React.FC<HamburgerMenuProps> = ({
onShowWineList,
onShowAddForm
}) => {
const { isOpen, toggleMenu, closeMenu, menuRef } = useHamburgerMenu();
const handleMenuItemClick = (action: () => void) => {
action();
closeMenu();
};
return (
<div className="hamburger-menu" ref={menuRef}>
{/* ... */}
</div>
);
};
よくあるミスと対策
1. イベントリスナーのクリーンアップ忘れ
// ❌ 悪い例:クリーンアップなし
useEffect(() => {
document.addEventListener('keydown', handleEscKey);
}, [isOpen]);
// ✅ 良い例:クリーンアップ関数を返す
useEffect(() => {
if (isOpen) {
document.addEventListener('keydown', handleEscKey);
}
return () => {
document.removeEventListener('keydown', handleEscKey);
};
}, [isOpen]);
2. 状態更新のタイミング
// ❌ 悪い例:非同期処理の考慮なし
const handleMenuItemClick = (action: () => void) => {
setIsOpen(false);
action(); // メニューが閉じる前に実行される可能性
};
// ✅ 良い例:順序を保証
const handleMenuItemClick = (action: () => void) => {
action();
setIsOpen(false);
};
パフォーマンス最適化
useCallbackでメモ化
関数の再生成を防ぎます。
import { useCallback } from 'react';
const toggleMenu = useCallback(() => {
setIsOpen(prev => !prev);
}, []);
const handleMenuItemClick = useCallback((action: () => void) => {
action();
setIsOpen(false);
}, []);
まとめ
シンプルにできました

Reactでハンバーガーメニューを実装する際のポイント:
基本実装:
-
useStateで開閉状態を管理 - 条件付きレンダリングでメニューを表示/非表示
- オーバーレイクリックで閉じる UX
アクセシビリティ:
-
aria-labelでスクリーンリーダー対応 - キーボード操作(Escキー)のサポート
- 適切なセマンティックHTML
高度な機能:
- 外部クリック検知
- スクロール制御
- カスタムフック化
この実装パターンは、モバイルメニューだけでなく、ドロップダウンやモーダルなど、様々なUIコンポーネントに応用できます。
Discussion