これまでの Next.js ブログサイト構築シリーズをまとめたページは以下の通りです。
Next.js でブログを構築していると、以下の課題に直面する場合があります。
「PC で見ると問題ないのに、スマホで見るとナビゲーションが崩れている……」
私のブログでも、ヘッダーに「HOME」「Next.js」「Tech」など7つのリンクを並べた結果、
スマートフォンの狭い画面では項目が横に収まりきらず、表示がおかしくなっていました。
今回はこの問題を解決するために、
スマートフォン専用の「ハンバーガーメニュー」を実装しました。
三本線のアイコン(≡)をタップするとメニューが展開し、× アイコンで閉じる、
スマホサイトでお馴染みの仕組みです。
この記事では、非エンジニアの方でも迷わず実装できるよう、
以下の内容を丁寧に解説します。
- Next.js の「サーバーコンポーネント」と「クライアントコンポーネント」の違い
useStateを使ったメニューの開閉制御- Tailwind CSS のブレークポイント(
md:)による表示切り替え - 変更後の
Header.tsxの全コード
パソコンとスマホでメニューの表示方法を変え、
スマホではハンバーガーメニューが開いたり、閉じたりする動きができるようになり、
ちゃんとした Web サイトっぽくなってきたと感じています。
前提条件
この記事は Next.js ブログ構築シリーズの第14回です。
第13回補足までの内容が完了していることを前提にしています。
- Next.js のバージョン: Next.js 16.2 系( React 19)環境の App Router を使用しています。
- 対象読者: シリーズ第13回補足までを完了し、コードブロックのシンタックスハイライトとコピーボタンが動作している方を対象としています。
シリーズの前回の記事はこちらです。
ハンバーガーメニューとは
ハンバーガーメニューとは、三本線のアイコン(≡)をタップするとナビゲーションが展開され、× アイコンをタップすると閉じる UI のことです。
スマートフォンのサイトでよく見かける形式で、ナビゲーション項目が多いときにコンパクトにまとめる手段として広く使われています。
名前の由来は、三本の横線がハンバーガーの断面図に見えるからです。
今回実装したハンバーガーメニューの動作は以下の通りです。
- PC(幅768px以上):従来通り、ナビゲーションを横並びで表示する
- スマホ(幅768px未満):三本線アイコンを表示し、タップするとメニューが縦に展開される
- リンクをタップすると、メニューが自動的に閉じる

上の画像は、ハンバーガーメニューが閉じている状態です。

上の画像は、ハンバーガーメニューが開いている状態です。
なぜ Header.tsx を「クライアントコンポーネント」にする必要があるのか
今回の実装に入る前に、Next.js(App Router)の非常に重要な概念を理解しておく必要があります。
コンポーネントとは、ヘッダーやフッターのように「画面の一部を構成する部品」のことです。
Next.js の部品(コンポーネント)には、大きく分けて2種類あります。
サーバーコンポーネント(デフォルト)
VPS(サーバー)側で HTML を組み立ててからブラウザに送る方式です。
ページの表示が速いという利点がありますが、「ボタンをクリックして画面を変化させる」といったユーザーの操作に反応する動きは作れません。
クライアントコンポーネント('use client')
ブラウザ側で JavaScript が動き、ユーザーの操作に反応できます。ボタンのクリックやメニューの開閉など、対話的な動作が必要なときに使います。
ハンバーガーメニューは「タップしたら開く・閉じる」という対話的な動作が必要なため、クライアントコンポーネントにする必要がありました。
また、後述する useState(開閉状態を記憶する機能)もクライアントコンポーネントでしか使えません。
ファイルの先頭に 'use client'; と書くことで、Next.js に「この部品はブラウザ側で動かしてください」と指示しています。
実装手順
STEP 1:Header.tsx を編集する
それでは、実際にコードを修正していきましょう。変更するファイルは ~/example-blog/components/Header.tsx です。
前回の第13回で作成したヘッダーに、メニューの開閉状態を記憶する「状態(State)」と、それを制御するボタンを追加します。
以下のコードをコピーして、現在の Header.tsx の内容をまるごと書き換えてください。
'use client';
import Link from 'next/link';
import { useState } from 'react';
// ナビゲーション用のリンクデータ(管理しやすいように配列化)
const navLinks = [
{ href: '/', label: 'HOME' },
{ href: '/category/nextjs', label: 'Next.js' },
{ href: '/category/tech', label: 'Tech' },
{ href: '/category/notes', label: 'Notes' },
{ href: '/about', label: 'About' },
{ href: 'https://lifework-blog.com', label: 'LIFEWORK Blog', external: true },
{ href: '/contact', label: 'お問い合わせ' },
];
// 個別のリンク部品
function NavLink({ href, label, external, onClick }: { href: string; label: string; external?: boolean; onClick?: () => void }) {
const commonProps = {
className: "block py-2 md:py-0 hover:text-[#4dd4b0] transition-colors",
onClick: onClick
};
if (external) {
return (
<a href={href} target="_blank" rel="noopener noreferrer" {...commonProps}>
{label}
</a>
);
}
return (
<Link href={href} {...commonProps}>
{label}
</Link>
);
}
export default function Header() {
// メニューが開いているかどうかを管理する状態(false = 閉じている)
const [isMenuOpen, setIsMenuOpen] = useState(false);
// メニューを閉じる関数
const closeMenu = () => setIsMenuOpen(false);
return (
<header className="bg-[#0a362e] text-white shadow-md sticky top-0 z-50">
<nav className="max-w-5xl mx-auto px-4">
<div className="flex flex-col">
{/* 上段:サイトタイトルとハンバーガーボタン */}
<div className="flex justify-between items-center h-16">
<Link href="/" className="text-xl font-bold tracking-tighter" onClick={closeMenu}>
LIFEWORK Blog<span className="text-[#4dd4b0] text-sm ml-1">Next</span>
</Link>
{/* スマホ用:メニュー開閉ボタン(md:hidden で PC では隠す) */}
<button
className="md:hidden p-2"
onClick={() => setIsMenuOpen(!isMenuOpen)}
aria-label="メニューを開閉"
>
{isMenuOpen ? (
// × アイコン
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
) : (
// 三本線アイコン(≡)
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
)}
</button>
</div>
{/* 下段:PC用ナビゲーション(hidden md:flex でスマホでは隠す) */}
<div className="hidden md:flex items-center space-x-6 h-10 border-t border-[#0a5242] text-sm">
{navLinks.map((link) => (
<NavLink key={link.href} {...link} />
))}
</div>
{/* スマホ用ドロップダウンメニュー:isMenuOpen が true のときだけ表示 */}
{isMenuOpen && (
<ul className="md:hidden border-t border-[#0a5242] pb-4 pt-2 space-y-1">
{navLinks.map((link) => (
<li key={link.href} className="px-2">
<NavLink {...link} onClick={closeMenu} />
</li>
))}
</ul>
)}
</div>
</nav>
</header>
);
}
STEP 2:ビルドとデプロイ
ファイルを保存したら、VPS 上でビルドとデプロイを行いましょう。
cd ~/example-blog
./deploy.sh
ビルドが完了したら、スマートフォン、または PC ブラウザの検証ツール(F12)を使って動作を確認してください。
コードの解説
navLinks 配列でリンクを一元管理する
const navLinks = [
{ href: '/', label: 'HOME' },
{ href: 'https://lifework-blog.com', label: 'LIFEWORK Blog', external: true },
...
];
ナビゲーションリンクを「配列」として定義しています。
配列とは複数のデータをまとめて管理する入れ物のことで、ここでは各リンクの情報を「オブジェクト({} で囲まれたデータのまとまり)」として1件ずつ書き、それを [] でまとめています。
PC 用・スマホ用の両方でこのリストを使い回すことで、将来リンクを追加・変更するときにこの1か所だけを編集すれば済むようにしています。
各オブジェクトの項目の意味は以下の通りです。
| 項目 | 意味 |
|---|---|
href | リンク先の URL |
label | 画面に表示するテキスト |
external | 外部リンク(別サイト)かどうか。true なら新しいタブで開く。省略した場合は内部リンクとして扱われる |
NavLink コンポーネントで重複をなくす
function NavLink({ href, label, external, onClick }) { ... }
リンク1件分を描画する部品を NavLink として定義し、PC 用・スマホ用の両方から呼び出す形にしました。
これにより「同じコードを2か所に書く」という状態を避け、修正漏れのリスクを減らしています。
コンポーネントの中では commonProps という変数に、全リンク共通のスタイルとクリック処理をまとめています。
const commonProps = {
className: "block py-2 md:py-0 hover:text-[#4dd4b0] transition-colors",
onClick: onClick
};
そして external の値によって、内部リンクか外部リンクかを切り替えています。
externalがtrueのとき:<a>タグを使い、新しいタブで開くexternalが指定されていないとき:Next.js のLinkコンポーネントを使って内部遷移する
呼び出す側のコードはこのようになっています。
<NavLink key={link.href} {...link} />
{...link} という書き方は「スプレッド構文」と呼ばれ、link オブジェクトの中身(href・label・external)をすべてまとめて NavLink に渡す省略記法です。
1つずつ href={link.href} label={link.label} ... と書く代わりに使えます。
また、リスト表示のときに key={link.href} を指定しています。
これは React が「どの項目が変更されたか」を判断するために必要な識別子で、同じリスト内で重複しない値(ここでは URL)を指定します。
useState でメニューの開閉を管理する
const [isMenuOpen, setIsMenuOpen] = useState(false);
const closeMenu = () => setIsMenuOpen(false);
useState は「状態を記憶する」ための機能です。
通常の変数は画面が再描画されるたびにリセットされますが、useState で管理した値は保持されます。
isMenuOpen:現在の状態を表す変数。false= 閉じている、true= 開いているsetIsMenuOpen:状態を変更するための関数。直接isMenuOpen = trueとは書けないため、必ずこの関数を使うuseState(false):最初の状態を「閉じている(false)」に設定しているcloseMenu:メニューを強制的にfalse(閉じた状態)にする関数。リンクタップ時に呼ばれる
ハンバーガーボタン自体の開閉は、ボタンの onClick に直接 setIsMenuOpen(!isMenuOpen) と書いています。!isMenuOpen は「現在の値を反転させる」という意味で、タップするたびに true ↔ false が切り替わります。
Tailwind CSS で PC 用・スマホ用を切り替える
{/* PC用:768px以上で表示、スマホでは隠す */}
<div className="hidden md:flex ...">
{/* スマホ用:768px未満で表示、PC では隠す */}
<button className="md:hidden ...">
Tailwind CSS には「ブレークポイント」という概念があり、画面幅に応じてスタイルを切り替えられます。md: というプレフィックスは「画面幅が768px以上のときだけこのスタイルを適用する」という意味です。
768px という数値は Tailwind CSS が md として定めているデフォルト値で、一般的なスマートフォンとタブレット・PC の境界線として広く使われている基準です。
hidden md:flex:デフォルトは非表示(hidden)、768px 以上では横並び(flex)にする → PC 用ナビゲーションmd:hidden:768px 以上では非表示にする → スマホ用ハンバーガーボタン
この組み合わせにより、同じヘッダーの中に PC 用とスマホ用の表示を共存させています。
{isMenuOpen && (...)} でメニューの表示を制御する
{isMenuOpen && (
<ul className="md:hidden ...">
...
</ul>
)}
isMenuOpen && (...) という書き方は、JavaScript の「短絡評価(ショートサーキット)」を使った条件付き表示のパターンです。
A && B は「A が true のときだけ B を返す」という意味で、ここでは「isMenuOpen が true(メニューが開いている)のときだけ <ul> を表示する」という動作になります。isMenuOpen が false のときはこの <ul> ごと DOM から消えるため、画面には何も表示されません。
ハンバーガーアイコンを SVG で直接書く理由
ハンバーガーアイコン(三本線と ×)は外部ライブラリを使わず、SVG で直接書いています。
アイコンのためだけに外部パッケージを追加するとプロジェクトの依存関係が増えてしまうため、今回は SVG 直書きを選びました。
三本線は d="M4 6h16M4 12h16M4 18h16" という座標指定で描き、× は d="M6 18L18 6M6 6l12 12" で描いています。
rel="noopener noreferrer" について
外部リンクには target="_blank" (新しいタブで開く)を指定していますが、合わせて rel="noopener noreferrer" も必ず設定しています。
これはセキュリティ上の対策で、リンク先のページからリンク元のページを JavaScript で操作されないようにするために必要です。
外部リンクを target="_blank" で開くときは常にセットで書くことが推奨されています。
動作確認
- 自分のブログ(例:
https://next.example.com)をブラウザで開く - PC(ブラウザ幅を広めにした状態)で以下を確認する
- ナビゲーションが横並びで表示されること
- ハンバーガーアイコンが表示されないこと
- Chrome の DevTools(F12)でスマホサイズに変更して以下を確認する
- 三本線アイコンが右上に表示されること
- タップするとメニューが縦に展開されること
- × アイコンをタップするとメニューが閉じること
- リンクをタップしたときもメニューが閉じること
まとめ
今回は、Next.js ブログにスマートフォン用のハンバーガーメニューを実装しました。
実装のポイントを振り返ると:
'use client':クライアントコンポーネントへの変更。対話的な動作が必要なときに使うuseState:画面の再描画をまたいで状態(開閉)を記憶する機能- Tailwind CSS のブレークポイント(
md:):768px を境に PC とスマホの表示を切り替える {条件 && <要素>}:条件が true のときだけ要素を表示する書き方
これで、どのデバイスからアクセスしても読みやすいブログの形が整いました。
デザインが整ってくると、記事を書くモチベーションも一段と上がりますね。
次回は Google アナリティクス( GA4 )と Google サーチコンソールの設定を行います。
どちらもデータの蓄積が始まるタイミングで設定しておかないと過去のデータが失われます。
サイトの外枠が整ったこのタイミングで、早めに設定しておきます。



コメント