



WithCodeMedia-1-pc
WithCodeMedia-2-pc
WithCodeMedia-3-pc
WithCodeMedia-4-pc




WithCodeMedia-1-sp
WithCodeMedia-2-sp
WithCodeMedia-3-sp
WithCodeMedia-4-sp









生徒スマホサイトでよく見る、あの3本線のハンバーガーメニューを自作したいんです。JavaScriptなしでも作れますか?
ペン博士作れるよ!実はHTMLとCSSだけで開いたり閉じたりできるんだ。3本線が×に変わるアニメも、スライドインも全部CSSでOK。もちろんJSを使う本格版も、この記事で最後まで一緒に作っていくね!
スマートフォンでWebサイトを見ると、画面の右上や左上に3本の横線が並んだアイコンが置いてあることがほとんどです。タップするとメニューがにゅっと開く、あの部品を「ハンバーガーメニュー」と呼びます。パンのバンズと具材を横から見た形に似ていることが名前の由来です。
「作るのが難しそう」「JavaScriptが必要なんでしょう?」と身構えてしまう方が多いのですが、実はHTMLとCSSだけで動くハンバーガーメニューを作れます。この記事では、まずJavaScriptを一切使わない純CSS版を完全なコードで組み立て、そのあとスライドインやオーバーレイなどのバリエーション、さらにJavaScript版、アクセシビリティ対応、レスポンシブ切り替えまで、一つずつコピペできる形で解説していきます。
コードはすべて動く状態で載せています。まずは何も考えずに貼り付けて動かし、そのあと「なぜ動くのか」を読んで理解を深めていくのがおすすめです。手を動かしながら読み進めてください。
最初に、この記事の結論をまとめておきます。
<input type="checkbox"> と <label>、そして :checked を組み合わせる「チェックボックスハック」が定番です。span の本体と ::before / ::after で3本の線を表現し、開いたときに×へ変形させます。aria-expanded の切り替えやEscキーで閉じる、外側クリックで閉じるといった配慮を入れやすいためです。button 要素の採用、キーボード操作、PCとスマホの表示切り替えまで整えて初めて実務レベルになります。ハンバーガーメニューとは、複数のナビゲーションリンクを1つのアイコンの中に隠しておき、タップ(クリック)で開閉できるようにしたUIパーツのことです。主にスマートフォンのように画面が狭い環境で、ナビゲーションの場所を節約する目的で使われます。
パソコンの画面は横に広いので、「ホーム」「サービス」「会社概要」「お問い合わせ」といったメニューを横一列に並べられます。しかしスマートフォンの縦長で狭い画面では、同じように並べると窮屈になり、コンテンツを表示する場所が減ってしまいます。そこで普段はメニューを隠しておき、必要なときだけ開くという発想が生まれました。それがハンバーガーメニューです。
たとえば横幅375pxほどのスマートフォンで、メニュー項目が5つも6つもあると、それだけで画面の上部が文字で埋め尽くされてしまいます。ユーザーが本当に読みたいのは記事や商品の情報であって、メニューではありません。ハンバーガーメニューは、限られた画面をコンテンツのために最大限あけるための、スマートフォン時代の合理的な解決策なのです。今ではほとんどのスマートフォン向けサイトで採用されている、いわば標準的なUIになっています。
なぜ3本の横線なのでしょうか。これは1980年代のコンピュータUIにさかのぼる歴史のあるアイコンで、リストの項目が並んでいる様子を線で抽象化したものだと考えられています。文字を書かなくても「ここに項目のリストがある」と直感的に伝わるため、世界中のサイトやアプリで共通の記号として定着しました。
線が3本なのは、少なすぎると「メニュー」だと伝わりにくく、多すぎると別の意味(設定やリストなど)に見えてしまうためです。3本がもっともバランスよく「開くメニュー」を表せる本数として使われ続けています。
ハンバーガーメニューの動作を分解すると、実はとてもシンプルです。「閉じている状態」と「開いている状態」の2つを切り替えているだけです。閉じているときはメニューを隠し、開いているときはメニューを見せる。この状態の切り替えを、CSSでやるかJavaScriptでやるかの違いだけなのです。
この記事では、まずCSSだけで状態を切り替える方法から始めます。状態を持つ仕組みとして、チェックボックスの「オン・オフ」を利用するのがポイントです。
先に完成イメージを共有しておきます。これから作るのは、次のような部品です。
これらを、まずはCSSだけで、次にJavaScriptでも実装していきます。それぞれ長所と短所があるので、両方を知っておくと現場で使い分けられます。
チェックボックスハックとは、画面上に隠したチェックボックスのオン・オフ状態を利用して、JavaScriptを使わずに開閉やタブ切り替えを実現するテクニックです。ハンバーガーメニューを純CSSで作る際の中心的な手法になります。
仕組みの核になるのは、次の3つの要素の組み合わせです。順番に役割を確認しましょう。
<input type="checkbox"> は、チェックが入っているか・いないかというオンとオフの2つの状態を自分で覚えてくれる部品です。この「状態を覚える」性質を、メニューの「開いている・閉じている」に対応させます。チェックが入っていれば開いている、入っていなければ閉じている、という具合です。
<label> は本来、入力欄に説明ラベルを付けるための要素ですが、labelをクリックすると、結びついたチェックボックスのオン・オフが切り替わるという便利な性質があります。label の for 属性に、チェックボックスの id と同じ値を指定すると両者が結びつきます。
この性質を使えば、チェックボックス自体は画面から隠しておき、代わりに3本線のアイコンを label にして「タップできるボタン」に見せられます。ユーザーはアイコンをタップしているつもりでも、裏ではチェックボックスが切り替わっているのです。
:checked は、チェックが入っている状態のときだけスタイルを当てられるCSSの疑似クラスです。これと、隣の要素を指す「兄弟結合子」を組み合わせます。
/* チェックが入ったとき、その後ろにあるメニューを表示する */
#menu-toggle:checked ~ .menu {
/* ここに「開いた状態」のスタイルを書く */
}
~(チルダ)は「同じ親を持つ、後ろにある兄弟要素」を指す記号です。+(プラス)は「すぐ隣の兄弟」だけを指しますが、~ は間に他の要素が挟まっていても後ろにあれば対象になります。この結合子のおかげで、チェックボックスの状態に応じて離れた場所のメニューを操作できるのです。
少し整理すると、CSSには要素どうしの位置関係を表す記号がいくつかあります。> は「直接の子」、+ は「すぐ次の兄弟」、~ は「後ろに続くすべての兄弟」を指します。ハンバーガーメニューでは、チェックボックスから見て後ろに置いたアイコンやメニューをまとめて操作したいので、~(後ろの兄弟すべて)が最適というわけです。ここを + にしてしまうと、間に別の要素が挟まった瞬間に効かなくなるので注意してください。
チェックボックスハックには1つだけ守るべきルールがあります。それはチェックボックスを、影響を与えたい要素より前(上)に置くことです。CSSの兄弟結合子は「後ろの兄弟」しか指せないため、順番を逆にすると動きません。HTMLでは「チェックボックス → ラベル → メニュー」の順に並べるのが基本形になります。
この前提を頭に入れたうえで、次の章から実際に完全なコードを組み立てていきます。
まずはHTMLの全体を作ります。ここではチェックボックス・ラベル(3本線)・メニュー本体の3点セットを、正しい順番で並べます。
<!-- ① 状態を覚えるチェックボックス(画面上は隠す) -->
<input type="checkbox" id="menu-toggle" class="menu-toggle">
<!-- ② 3本線のアイコン(labelでタップ可能に) -->
<label for="menu-toggle" class="menu-btn">
<span class="menu-btn-line"></span>
</label>
<!-- ③ 開くメニュー本体 -->
<nav class="menu">
<ul>
<li><a href="#">ホーム</a></li>
<li><a href="#">サービス</a></li>
<li><a href="#">実績紹介</a></li>
<li><a href="#">会社概要</a></li>
<li><a href="#">お問い合わせ</a></li>
</ul>
</nav>
ポイントを1つずつ確認します。
<input> を一番上に置く:後ろの兄弟要素(アイコンとメニュー)にCSSで影響を与えるためです。順番を逆にすると動きません。id と for を一致させる:id="menu-toggle" と for="menu-toggle" が同じ値なので、ラベルをタップするとチェックボックスが切り替わります。span 1つで作る:本体を真ん中の線にして、残り2本は擬似要素で作ります。詳しくはCSS編で解説します。<nav> でマークアップ:ナビゲーションであることを意味的に示すために nav と ul/li を使います。HTMLはこれだけです。この時点ではまだスタイルがないので、チェックボックスもリンクもそのまま画面に表示されています。次のCSS編で、一気にハンバーガーメニューらしい見た目と動きを付けていきます。
いよいよCSSです。長く見えますが、やっていることは「チェックボックスを隠す」「3本線を作る」「開いたときの見た目を指定する」の3ステップだけです。まず完成形の全コードを載せ、そのあとブロックごとに解説します。
/* ===== ① チェックボックス本体は画面から隠す ===== */
.menu-toggle {
display: none;
}
/* ===== ② 3本線ボタン(label)===== */
.menu-btn {
position: fixed;
top: 20px;
right: 20px;
z-index: 100;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
/* 真ん中の線 */
.menu-btn-line,
.menu-btn-line::before,
.menu-btn-line::after {
content: "";
display: block;
width: 28px;
height: 3px;
background: #333;
border-radius: 2px;
transition: all 0.3s ease;
}
/* 上下の線を擬似要素で作り、上下にずらす */
.menu-btn-line::before,
.menu-btn-line::after {
position: absolute;
}
.menu-btn-line::before { transform: translateY(-9px); }
.menu-btn-line::after { transform: translateY(9px); }
/* ===== ③ メニュー本体(初期状態=隠れている)===== */
.menu {
position: fixed;
top: 0;
right: 0;
width: 260px;
height: 100%;
background: #1e293b;
padding: 80px 24px;
box-sizing: border-box;
transform: translateX(100%); /* 画面右の外へ隠す */
transition: transform 0.3s ease;
z-index: 90;
}
.menu ul {
list-style: none;
margin: 0;
padding: 0;
}
.menu li { margin-bottom: 8px; }
.menu a {
display: block;
padding: 12px 8px;
color: #fff;
text-decoration: none;
border-radius: 6px;
transition: background 0.2s;
}
.menu a:hover { background: rgba(255,255,255,0.1); }
/* ===== ④ チェックが入ったとき(=開いたとき)===== */
/* メニューを画面内へスライドイン */
.menu-toggle:checked ~ .menu {
transform: translateX(0);
}
/* 真ん中の線を消す */
.menu-toggle:checked ~ .menu-btn .menu-btn-line {
background: transparent;
}
/* 上下の線を回転させて×にする */
.menu-toggle:checked ~ .menu-btn .menu-btn-line::before {
transform: translateY(0) rotate(45deg);
}
.menu-toggle:checked ~ .menu-btn .menu-btn-line::after {
transform: translateY(0) rotate(-45deg);
}
上のコードで、タップするとメニューが右からスライドインし、3本線が×に変わるハンバーガーメニューが完成します。行数は多いですが、丸暗記する必要はありません。「隠す」「線を作る」「開いた状態を書く」の3ステップに分けて眺めれば、意外と単純だと分かるはずです。ここから各ブロックを分解して理解していきましょう。読んだあとにもう一度コード全体を見返すと、それぞれの行が何をしているかがはっきりつかめるはずです。
.menu-toggle { display: none; } でチェックボックスの四角い見た目を画面から消しています。ただし要素自体は存在し続けるので、チェック状態は変わらず記憶されます。ユーザーには見えなくても、裏でオン・オフのスイッチとして働いているのです。
なお display: none; で完全に消すと、キーボードのTabキーで選べなくなります。アクセシビリティを重視する場合は後述の方法で画面外へ逃がすやり方もありますが、まずは動きを理解するためにシンプルに隠しています。
ここがCSSの見せ場です。3本の線を、それぞれ別のHTML要素で用意するのではなく、1つのspanを真ん中の線にして、上下の線は::beforeと::afterという擬似要素で生成しています。擬似要素とは、CSSだけで「見えない要素を1つ追加する」機能です。
content: ""; を指定すると擬似要素が実体を持ちます。3本とも同じ幅・高さ・色にし、上の線は translateY(-9px) で上へ、下の線は translateY(9px) で下へずらすことで、等間隔の3本線が完成します。transition を全体にかけているので、後述の変形がなめらかにアニメーションします。
メニュー本体は position: fixed; で画面に固定し、transform: translateX(100%); で自分の幅のぶんだけ右へずらして画面外に追い出しています。display: none; ではなく transform で外に出す理由は、位置の変化をアニメーションさせられるからです。display の切り替えはアニメーションできません。
最後に、チェックが入ったときのスタイルをまとめて書きます。.menu-toggle:checked ~ .menu でメニューのtransformをtranslateX(0)に戻して画面内へ滑り込ませ、同時に真ん中の線を透明にし、上下の線を rotate(45deg) と rotate(-45deg) で回転させて×印を作っています。
translateY を 0 に戻してから回転させるのがコツです。これで上下の線が中央に集まり、45度と-45度で交差して×になります。transition のおかげで、線がぐるっと回りながら×に変わるアニメーションになります。
初心者が一番「おっ」と感じるのが、3本線が×に変わる動きです。ここではその変形が起きる仕組みを座標レベルで理解し、思いどおりに調整できるようにします。
×印は2本の斜め線でできています。つまり×を作るには線が2本あれば足りるのです。3本のうち真ん中の1本は余るので、background: transparent; で見えなくしています。display: none; ではなく透明にするのは、消える瞬間もアニメーションでふわっと見せるためです。
閉じている状態では、上の線は -9px、下の線は 9px の位置にあります。開いた瞬間、両方を translateY(0) にしていったん中央の同じ高さに重ねてから、片方を +45度、もう片方を -45度 回転させます。すると2本が交差して×になるわけです。移動と回転を1つの transform にまとめて書くのがポイントです。
/* 移動と回転はスペース区切りで同時に指定できる */
.menu-toggle:checked ~ .menu-btn .menu-btn-line::before {
transform: translateY(0) rotate(45deg);
}
注意点として、transform は複数の値を書くと右から順に適用される性質があります。translateY(0) rotate(45deg) の場合は「回転してから移動」ではなく「移動してから回転」に見えますが、値の順序を変えると軸のずれ方が変わるので、思いどおりに交差しないときは順序を入れ替えて調整してください。
動きの印象は transition の値で決まります。0.3s の部分を大きくするとゆっくり、小さくするとキビキビ動きます。ease の部分を cubic-bezier() に変えると、跳ねるような独特の動きも作れます。
/* ゆっくり・弾むような変形にしたいとき */
.menu-btn-line,
.menu-btn-line::before,
.menu-btn-line::after {
transition: all 0.45s cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
動きにこだわりすぎると読み込みが重くなったり、酔いやすいユーザーに負担をかけたりします。速さは 0.3s 前後を基準に、控えめに設定するのが無難です。
メニューの「開き方」は好みや設計に合わせて選べます。ここでは代表的な6パターンを紹介します。どれも先ほどのHTMLはそのまま使い、.menuまわりのCSSを差し替えるだけで切り替えられます。
すでに作った、画面の右端からメニューが滑り込むタイプです。もっとも定番で失敗が少ないので、迷ったらこれを選べば問題ありません。
.menu {
right: 0;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.menu-toggle:checked ~ .menu {
transform: translateX(0);
}
左端から出したいときは、位置を左に変え、隠す方向を逆にします。translateX(-100%)で左の外へ隠すのがポイントです。
.menu {
left: 0;
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.menu-toggle:checked ~ .menu {
transform: translateX(0);
}
スライドではなく、その場で透明度だけを変えて浮かび上がらせる落ち着いた演出です。opacity と visibility を組み合わせます。visibility を切り替えるのは、透明でもリンクを押せてしまう状態を防ぐためです。
.menu {
right: 0;
transform: none;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.menu-toggle:checked ~ .menu {
opacity: 1;
visibility: visible;
}
メニューで画面いっぱいを覆うインパクトのあるタイプです。ブランドサイトなどでよく見かけます。幅と高さを100%にし、リンクを中央に大きく配置します。
.menu {
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(15, 23, 42, 0.97);
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
}
.menu ul { text-align: center; }
.menu a { font-size: 24px; }
.menu-toggle:checked ~ .menu {
opacity: 1;
visibility: visible;
}
ヘッダーの下からにゅっと下方向に降りてくるタイプです。translateY(-100%) で上の外へ隠しておき、開いたときに translateY(0) で降ろします。
.menu {
top: 0;
left: 0;
width: 100%;
height: auto;
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.menu-toggle:checked ~ .menu {
transform: translateY(0);
}
メニューが本文の上に重なるのではなく、本文全体を横に押しやってメニューのスペースを空けるタイプです。ページ全体を包むラッパー要素を1つ用意し、その transform を動かします。HTMLの構造だけ少し変わる点に注意してください。
<!-- メニュー以外の全コンテンツを .page で包む -->
<input type="checkbox" id="menu-toggle" class="menu-toggle">
<label for="menu-toggle" class="menu-btn"><span class="menu-btn-line"></span></label>
<nav class="menu"> ... </nav>
<div class="page">
<!-- ここにページ本文が入る -->
</div>
.page {
transition: transform 0.3s ease;
}
.menu-toggle:checked ~ .page {
transform: translateX(-260px); /* メニュー幅ぶん左へ */
}
プッシュ型は動きが独特で印象的ですが、本文がずれるぶんレイアウト崩れが起きやすいので、横スクロールが出ないよう body { overflow-x: hidden; } を添えておくと安全です。
演出が派手なほど良いわけではありません。情報を素早く見せたいメディアやコーポレートサイトなら右スライドやドロップが無難で、世界観を打ち出したいブランドサイトならフルスクリーンオーバーレイが映えます。用途に合わせて選んでください。
| 開き方 | 印象 | 向いているサイト |
|---|---|---|
| 右/左スライド | 定番・軽快 | コーポレート・メディア |
| フェード | 落ち着き・上品 | ポートフォリオ・士業 |
| フルスクリーン | インパクト大 | ブランド・LP |
| ドロップダウン | 自然・素直 | 情報量の多いサイト |
| プッシュ | 独特・遊び | 個性を出したいサイト |
メニューを開いたとき、背後のコンテンツを半透明の黒で覆うと、メニューが浮き立ってプロらしい仕上がりになります。この暗幕は「オーバーレイ」と呼ばれ、CSSだけでも作れます。
HTMLに、背景を覆うための空の要素を1つ加えます。これも label にしておくと、暗幕をタップしてメニューを閉じる機能まで同時に手に入ります。
<input type="checkbox" id="menu-toggle" class="menu-toggle">
<label for="menu-toggle" class="menu-btn"><span class="menu-btn-line"></span></label>
<label for="menu-toggle" class="overlay"></label> <!-- 暗幕 -->
<nav class="menu"> ... </nav>
.overlay {
position: fixed;
inset: 0; /* top/right/bottom/left をまとめて0に */
background: rgba(0, 0, 0, 0.5);
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease;
z-index: 80; /* メニューより下、本文より上 */
}
.menu-toggle:checked ~ .overlay {
opacity: 1;
visibility: visible;
}
inset: 0; は top: 0; right: 0; bottom: 0; left: 0; をまとめて書ける便利な指定で、画面いっぱいに広げられます。z-index はメニュー(90)より小さく、本文より大きい値(80)にして、暗幕がメニューの下・本文の上に重なるよう調整します。
この暗幕は「開いていることが分かりやすくなる」だけでなく、「どこをタップすれば閉じられるか」をユーザーに直感的に伝える役割も果たします。純CSS版でも label を使えば外側タップで閉じられるようになるのは大きな利点です。
ここまでで純CSS版が一通り完成しました。次の章からJavaScript版に進みますが、その前に2つの方式の違いと使い分けの基準をはっきりさせておきましょう。どちらが優れているという話ではなく、場面によって向き不向きがあります。
まずは特徴を表で比べてみます。自分が作ろうとしているサイトに、どちらが合うかを考えながら読んでください。
| 観点 | 純CSS版 | JavaScript版 |
|---|---|---|
| 作りやすさ | 手軽・行数少なめ | コードは増える |
| 表示速度 | 軽い(JS読み込み不要) | わずかに重い |
| Escキーで閉じる | 苦手 | 得意 |
| 外側クリックで閉じる | 工夫が必要 | 得意 |
| リンク選択で閉じる | 苦手 | 得意 |
| 状態の読み上げ対応 | 限定的 | 細かく制御できる |
| 向いている規模 | 小規模・学習用 | 本格的なサイト |
表を見ると、シンプルさなら純CSS版、機能の細かさならJavaScript版という傾向がはっきりします。純CSS版はJavaScriptを1行も書かずに動くので、学習の最初の一歩や、機能が最小限でよい個人サイトに向いています。
迷ったときは、次の順番で考えると決めやすいです。
実務で納品するサイトなら、アクセシビリティと操作性を優先してJavaScript版を選ぶことがほとんどです。一方、学習中の方はまず純CSS版で仕組みを理解してから、JavaScript版へ発展させるのが遠回りに見えて一番の近道です。この記事の順番どおりに進めれば、その流れを自然にたどれます。
ここからはJavaScript版です。純CSS版は手軽ですが、Escキーで閉じる・外側クリックで閉じる・状態を読み上げソフトに正しく伝えるといった細かな制御はJavaScriptのほうが得意です。実務ではこちらを使う場面が多いので、しっかり押さえましょう。
JS版では、チェックボックスの代わりに本物のボタンである<button>要素を使います。ボタンはキーボードで押せる、読み上げソフトが「ボタン」と認識するなど、最初からアクセシビリティに優れているためです。
<button class="menu-btn" aria-label="メニューを開く"
aria-expanded="false" aria-controls="global-menu">
<span class="menu-btn-line"></span>
</button>
<nav class="menu" id="global-menu">
<ul>
<li><a href="#">ホーム</a></li>
<li><a href="#">サービス</a></li>
<li><a href="#">お問い合わせ</a></li>
</ul>
</nav>
属性の意味は後半のアクセシビリティ章で詳しく説明します。ここでは aria-expanded(開いているか)と aria-controls(どのメニューを操作するか)を付けている、とだけ覚えておいてください。
JS版では、:checked の代わりに.is-openというクラスの有無で見た目を切り替えます。JavaScriptがこのクラスを付けたり外したりします。
.menu {
position: fixed;
top: 0; right: 0;
width: 260px;
height: 100%;
background: #1e293b;
transform: translateX(100%);
transition: transform 0.3s ease;
}
/* クラスが付いたら開く */
.menu.is-open {
transform: translateX(0);
}
/* ボタン側の×変形も同じくクラスで */
.menu-btn.is-open .menu-btn-line { background: transparent; }
.menu-btn.is-open .menu-btn-line::before { transform: translateY(0) rotate(45deg); }
.menu-btn.is-open .menu-btn-line::after { transform: translateY(0) rotate(-45deg); }
いよいよJavaScriptです。中心になるのはclassList.toggle()という命令で、クラスが付いていなければ付け、付いていれば外す、という切り替えを1行で行えます。
const btn = document.querySelector(".menu-btn");
const menu = document.querySelector(".menu");
btn.addEventListener("click", () => {
// ボタンとメニューの両方に is-open を付け外し
const isOpen = menu.classList.toggle("is-open");
btn.classList.toggle("is-open", isOpen);
// 状態を支援技術に伝える
btn.setAttribute("aria-expanded", String(isOpen));
btn.setAttribute("aria-label", isOpen ? "メニューを閉じる" : "メニューを開く");
});
toggle() は実行後にクラスが付いているかどうかをtrue/falseで返してくれます。この戻り値を isOpen に受け取り、aria-expanded の値やラベルの文言を同時に更新しているのがポイントです。これで見た目と「意味」の両方が正しく切り替わります。
キーボードだけで操作する人のために、Escキーでメニューを閉じられるようにしましょう。開いているときだけEscを受け付けます。
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && menu.classList.contains("is-open")) {
closeMenu();
}
});
function closeMenu() {
menu.classList.remove("is-open");
btn.classList.remove("is-open");
btn.setAttribute("aria-expanded", "false");
btn.setAttribute("aria-label", "メニューを開く");
btn.focus(); // フォーカスをボタンに戻す
}
閉じる処理は複数の場所(Escキー・外側クリック・リンク選択)から呼ばれるので、closeMenu() という関数にまとめて再利用しています。閉じた後に btn.focus() でフォーカスをボタンへ戻すのは、キーボード利用者が操作の続きをしやすくするための配慮です。
メニューの外側をクリックしたときに閉じる挙動も、多くのサイトで採用されています。クリックされた場所がメニューでもボタンでもなければ閉じる、という判定で実現します。
document.addEventListener("click", (e) => {
const clickedInside = menu.contains(e.target) || btn.contains(e.target);
if (!clickedInside && menu.classList.contains("is-open")) {
closeMenu();
}
});
contains() は「その要素の内側に、指定した要素が含まれるか」を判定するメソッドです。e.target(クリックされた実際の要素)がメニュー内でもボタン内でもなければ、外側とみなして閉じます。ボタン自身のクリックを除外しないと開いた瞬間に即閉じてしまうので、必ずボタンも判定に含めてください。
アクセシビリティとは、目が見えにくい人・マウスが使えない人・音声で操作する人など、あらゆる利用者が同じように使えるようにする配慮のことです。ハンバーガーメニューは操作の起点になる重要な部品なので、ここを丁寧に作ると一気にプロらしくなります。
クリックできる部品を <div> や <span> で作ってしまう人が多いのですが、これはNGです。開閉を切り替えるボタンは必ず<button>要素で作りましょう。button は次の性質を最初から備えています。
div で同じことをやろうとすると、tabindex やキー操作の処理を自分で書かなければならず、抜け漏れが起きがちです。最初から button を使えば、これらがすべて無料で手に入ります。
読み上げソフトに「このボタンが何を操作し、いま開いているのか閉じているのか」を伝えるのがaria-controlsとaria-expandedです。
| 属性 | 役割 | 値の例 |
|---|---|---|
| aria-controls | 操作対象のメニューのidを指す | global-menu |
| aria-expanded | いま開いているか | true / false |
| aria-label | ボタンの説明文 | メニューを開く |
重要なのは、aria-expanded を開閉に合わせてJavaScriptで必ず更新することです。見た目だけ変えて属性を放置すると、読み上げソフトを使う人には「閉じたまま」に聞こえてしまい、状態が食い違います。前章のコードで setAttribute("aria-expanded", ...) を入れていたのはこのためです。
マウスを使わない利用者のために、キーボードだけで開く・選ぶ・閉じるが完結することを確認します。チェック項目は次のとおりです。
buttonなら自動で対応)画面の動きで気分が悪くなる人のために、OSには「視差効果を減らす」という設定があります。この設定をしている人にはアニメーションを控えるのが親切です。CSSの prefers-reduced-motion で対応できます。
@media (prefers-reduced-motion: reduce) {
.menu,
.menu-btn-line,
.menu-btn-line::before,
.menu-btn-line::after {
transition: none;
}
}
この指定を入れておくと、動きを減らす設定の人にはアニメーションなしで即座に切り替わり、それ以外の人にはこれまでどおりなめらかに動きます。両方に配慮できる、覚えておきたいテクニックです。
メニューを開いたまま背景がスクロールできてしまうと操作しづらいので、開いている間だけbodyのスクロールを固定します。JavaScriptで body にクラスを付けて制御します。
/* CSS:この状態のときスクロールを止める */
body.menu-open {
overflow: hidden;
}
// JS:開閉に合わせて body のクラスも切り替える
document.body.classList.toggle("menu-open", isOpen);
overflow: hidden; を body に付けると、スクロール操作を受け付けなくなります。閉じるときにクラスを外すのを忘れるとページ全体がスクロールできなくなるので、開閉の両方で確実に切り替えてください。
実際のサイトでは、PCでは横並びのナビゲーション、スマホではハンバーガーメニューというように、画面幅で表示を切り替えるのが一般的です。この切り替えはCSSの「メディアクエリ」で行います。
メディアクエリとは、画面幅などの条件に応じて適用するCSSを切り替える仕組みです。@media (max-width: 768px) と書くと「画面幅が768px以下のときだけ」という意味になります。
/* --- PC(初期状態):ナビは横並び、ハンバーガーは隠す --- */
.menu {
position: static;
transform: none;
width: auto;
height: auto;
background: transparent;
}
.menu ul { display: flex; gap: 24px; }
.menu a { color: #333; }
.menu-btn { display: none; } /* PCでは3本線を消す */
/* --- スマホ(768px以下):ハンバーガー表示に切り替え --- */
@media (max-width: 768px) {
.menu-btn { display: flex; } /* 3本線を表示 */
.menu {
position: fixed;
top: 0; right: 0;
width: 260px;
height: 100%;
background: #1e293b;
transform: translateX(100%);
transition: transform 0.3s ease;
}
.menu ul { display: block; }
.menu a { color: #fff; }
.menu.is-open { transform: translateX(0); }
}
考え方はシンプルです。PC用のスタイルを基本として書き、スマホ用の変更点だけをメディアクエリの中で上書きしています。PCではハンバーガーボタンを display: none; で隠し、メニューを普通の横並びナビとして見せます。スマホになったらボタンを表示し、メニューを画面外へ隠すハンバーガー仕様に切り替えるわけです。
切り替える境目の幅を「ブレイクポイント」と呼びます。768pxが定番で、タブレット縦向きの境目にあたります。ナビの項目が多くて横並びに収まらない場合は、900pxや1024pxなど大きめにするのも手です。実際にブラウザの幅を縮めながら、横並びが窮屈になり始める幅を探して決めましょう。
メディアクエリには max-width(〜以下)と min-width(〜以上)の2つがあります。「スマホを基本に、PCで上書きする」設計ならmin-width、「PCを基本にスマホで上書きする」設計ならmax-widthを使います。どちらでも作れますが、近年は軽いスマホ表示を基本にする「モバイルファースト」の考え方から min-width が好まれます。
/* モバイルファースト:スマホが基本、PCで上書き */
.menu-btn { display: flex; } /* スマホ:ボタン表示 */
@media (min-width: 769px) {
.menu-btn { display: none; } /* PC:ボタンを隠す */
.menu ul { display: flex; } /* PC:横並び */
}
どちらの書き方でも結果は同じにできます。プロジェクトの方針に合わせ、チーム内で統一するのが一番大切です。
コードが動くようになったら、次は「使いやすく、迷わせない」ための見た目の調整です。ハンバーガーメニューは便利な反面、使い方を誤ると「メニューがどこにあるか分からない」という不満を生みやすい部品でもあります。この章ではまずUXのコツを押さえ、後半で実戦的なヘッダーの完成例まで組み立てます。
3本線だけだと、Webに不慣れな人には「これがメニューだ」と伝わりにくいことがあります。アイコンの下や横に小さく「MENU」と文字を添えるだけで、分かりやすさが大きく上がります。研究でも、文字ラベル付きのほうがタップ率が高い傾向が知られています。
<button class="menu-btn" aria-label="メニュー" aria-expanded="false">
<span class="menu-btn-line"></span>
<span class="menu-btn-text">MENU</span>
</button>
.menu-btn {
flex-direction: column;
gap: 4px;
}
.menu-btn-text {
font-size: 10px;
letter-spacing: 0.05em;
color: #333;
}
スマートフォンは指で操作するため、ボタンの当たり判定は最低44px四方を確保するのが目安です。3本線の見た目は細くても、button の width/height や padding を広げて、押しやすい大きさにしましょう。当たり判定が小さいと「押したのに反応しない」というストレスの原因になります。
メニューが開いたとき、3本線が×に変わる・背景が暗くなるといった変化をきちんと付けると、ユーザーは「いま開いている」「もう一度押せば閉じる」と直感的に理解できます。開閉の状態がひと目で分からないと、同じボタンを何度も押してしまう混乱を招きます。この記事で作った×への変形と暗幕は、まさにこの分かりやすさのための工夫です。
3本線やメニュー内の文字は、背景との明るさの差(コントラスト)を十分に取ることが大切です。薄いグレーの線を白い背景に置くと、目が疲れている人や明るい屋外では見えにくくなります。文字と背景のコントラスト比は、一般的なアクセシビリティ指針で本文なら4.5対1以上が推奨されています。濃い色の線・はっきりした文字色を選びましょう。
凝った動きは楽しいですが、開閉のたびに派手なアニメが走ると、かえって操作のテンポを損ねます。メニューは「素早く開いて、素早く使える」ことが最優先です。transition は0.3秒前後を基準に、控えめでキビキビした動きにまとめると、上品で使いやすい印象になります。前述の prefers-reduced-motion 対応も忘れずに入れておきましょう。
ここまでは部品を単体で作ってきました。ここからは、ロゴ・ハンバーガーボタン・メニューを1つのヘッダーとしてまとめた実戦的な完成例を組み立てます。コピーして自分のサイトの土台にできる形にしてあります。
ヘッダーは「ロゴ」と「ナビゲーション」の2つで構成するのが基本です。<header>の中に、左側にロゴ、右側にハンバーガーボタンとメニューを配置します。意味のあるHTMLタグを使うことで、検索エンジンや読み上げソフトにも構造が正しく伝わります。
<header class="site-header">
<a href="/" class="logo">WithCode</a>
<button class="menu-btn" aria-label="メニューを開く"
aria-expanded="false" aria-controls="global-menu">
<span class="menu-btn-line"></span>
</button>
<nav class="menu" id="global-menu">
<ul>
<li><a href="/">ホーム</a></li>
<li><a href="/service/">サービス</a></li>
<li><a href="/works/">実績紹介</a></li>
<li><a href="/about/">会社概要</a></li>
<li><a href="/contact/">お問い合わせ</a></li>
</ul>
</nav>
</header>
<header> はページの見出し領域を表す要素で、<nav> はナビゲーションを表す要素です。この2つを使うだけで、「ここはヘッダーで、この中にナビゲーションがある」という意味が機械にも伝わります。class 名は自由に付けて構いませんが、役割が分かる名前にしておくと後から見返したときに理解しやすくなります。
まずはPCでの見た目を整えます。ロゴを左、ナビを右に配置し、display: flex;とjustify-content: space-between;で両端に振り分けます。ハンバーガーボタンはPCでは不要なので隠しておきます。
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 32px;
background: #fff;
box-shadow: 0 1px 6px rgba(0,0,0,0.08);
position: sticky;
top: 0;
z-index: 50;
}
.logo {
font-size: 22px;
font-weight: bold;
color: #1e293b;
text-decoration: none;
}
.menu ul {
display: flex;
gap: 28px;
list-style: none;
margin: 0;
padding: 0;
}
.menu a {
color: #333;
text-decoration: none;
font-weight: 600;
}
.menu a:hover { color: #2563eb; }
.menu-btn { display: none; } /* PCでは隠す */
position: sticky; top: 0; を指定すると、スクロールしてもヘッダーが画面上部に貼り付いたままになります。ナビゲーションが常に見えるので、ユーザーがいつでもメニューにアクセスできて便利です。ヘッダーの z-index は50にして、本文より手前・開いたメニューより奥に来るよう調整しています。
次に、スマホ幅になったときのスタイルをメディアクエリで上書きします。ハンバーガーボタンを表示し、ナビを画面外へ隠すハンバーガー仕様に切り替えます。
@media (max-width: 768px) {
.site-header { padding: 12px 20px; }
.menu-btn { display: flex; }
.menu {
position: fixed;
top: 0; right: 0;
width: 260px;
height: 100%;
background: #1e293b;
padding: 80px 24px;
box-sizing: border-box;
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 90;
}
.menu ul { display: block; }
.menu li { margin-bottom: 8px; }
.menu a {
display: block;
padding: 12px 8px;
color: #fff;
border-radius: 6px;
}
.menu.is-open { transform: translateX(0); }
}
これで、PCでは横並びナビ、スマホではハンバーガーメニューという1つのヘッダーで両対応する完成形ができあがります。あとは前章のJavaScript(classList.toggleとcloseMenu)をそのまま組み合わせれば、開閉・Escキー・外側クリックまで動くようになります。パーツがそろったら、実際に自分のサイトへ貼り付けて、幅を変えながら動作を確かめてみましょう。
完成例を土台に、現場でよく行う微調整を3つ紹介します。どれも数行の追加で印象を変えられます。
position: absolute; left: 50%; transform: translateX(-50%); でロゴを中央固定にします。backdrop-filter: blur(8px); と半透明背景を組み合わせると、すりガラス風の今どきな見た目になります。li に transition-delay を少しずつずらして与えると、リンクが1つずつ現れる演出になります。「メニューを隠すと、検索エンジンにリンクが読まれず順位が下がるのでは?」という不安をよく聞きます。結論から言うと、正しく作られたハンバーガーメニューはSEOに悪影響を与えません。ここではその理由と、気をつけるべきポイントを整理します。
検索エンジンのクローラー(ページを読み取るプログラム)は、CSSで見た目が隠れているだけのリンクも、HTMLに存在していればきちんと読み取ります。この記事で作ったハンバーガーメニューは、<nav> の中に <a> リンクが最初からHTMLに書かれている状態です。transform で画面外に置いているだけなので、クローラーはリンクを問題なくたどれます。
逆に注意が必要なのは、JavaScriptで後からリンクを生成する作りです。HTMLに最初はリンクが無く、クリックして初めてリンクが作られる場合、クローラーが読み取れないことがあります。この記事のように、リンクは最初からHTMLに書いておくのが安全です。
SEOで大切なのは、ページの構造が意味的に正しいことです。ナビゲーションは <nav>、リストは <ul>/<li>、リンクは <a> と、役割に合ったタグを使いましょう。<div> だけで組むより、検索エンジンが構造を理解しやすくなります。この記事のコードはすべてこの原則に沿っています。
検索順位には表示速度も影響します。純CSS版はJavaScriptを読み込まないぶん、わずかに速いという利点があります。JavaScript版でも、コードは軽量なので大きな負担にはなりませんが、不要なライブラリを読み込まず、この記事のような素のJavaScriptで済ませることが、速度の面でもプラスに働きます。装飾用の大きな画像アイコンより、CSSで線を描くほうが軽いのも同じ理由です。
最後に、実際に作っていると必ずと言っていいほど遭遇する失敗パターンと、その原因・直し方をまとめます。ここを知っておくと、うまく動かないときの原因切り分けが一気に速くなります。
純CSS版で、四角いチェックボックスがそのまま表示されてしまうケースです。原因はdisplay: none;を書き忘れていること。.menu-toggle { display: none; } を確認してください。アクセシビリティを重視して要素を残したい場合は、次のように画面外へ逃がす方法もあります。
/* 見た目は消すが、キーボードでは選べる状態を保つ */
.menu-toggle {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
overflow: hidden;
}
メニューを開いたのに、ヘッダーや画像の後ろに潜り込んで見えないケースです。原因はほぼz-indexの指定不足です。z-index は要素の重なり順を決める値で、大きいほど手前に来ます。メニューやボタンには z-index: 90 以上など十分大きい値を与え、暗幕はそれより少し小さくして、正しい重なり順を作りましょう。
なお z-index は position が static 以外(fixed/absolute/relative など)のときだけ効きます。効かないときは position の指定を見直してください。
メニューを開いたまま背後のページがスクロールできてしまい、操作しづらい問題です。これは「スクロール貫通」と呼ばれます。前述のとおり、開いている間だけbodyにoverflow: hidden;を当てることで解決します。純CSS版なら次のように書けます。
/* 純CSS版:開いている間 html/body のスクロールを止める */
.menu-toggle:checked ~ .menu {
overflow-y: auto; /* メニュー自体は中身をスクロール可 */
}
純CSSだけでは body のスクロールを厳密に止めるのが難しい場面もあります。スクロール制御まで確実にやりたいなら、素直にJavaScript版で body のクラスを切り替えるのが確実です。
3本線が×にならず、線が変な位置にずれてしまうケースです。原因の多くは回転前にtranslateY(0)で中央へ戻していないことです。上下の線を回転させる前に、いったん中央の同じ高さに集める必要があります。transform: translateY(0) rotate(45deg); のように、移動を先に書いて中央へ戻してから回転させてください。
メニュー内のリンクをタップしてページ内を移動したのに、メニューが開いたままになるケースです。JS版ならリンクをタップしたときにもメニューを閉じる処理を足せば解決します。
// メニュー内のすべてのリンクに「閉じる」を仕込む
menu.querySelectorAll("a").forEach((link) => {
link.addEventListener("click", closeMenu);
});
純CSS版の場合は、リンク自体を label for="menu-toggle" にはできないので、この挙動は苦手です。リンク選択で閉じたいならJS版が向いています。用途に応じて選びましょう。
WithCode編集部でハンバーガーメニューを実装するときに、公開前に必ず確認している独自のチェックリストを共有します。作り終えたら1つずつ確認してみてください。
aria-expandedが開閉に合わせてtrue/falseに変わるかz-indexの重なりでメニューが隠れていないかこの8項目を通せば、見た目だけでなく操作性・アクセシビリティまで含めて「実務で通用する」ハンバーガーメニューになります。
ハンバーガーメニューを作るときに、初心者からよく寄せられる質問と回答をまとめました。
A. 学習や小規模なサイトなら純CSS版、本格的なサイトならJavaScript版がおすすめです。純CSS版は手軽で軽く、JavaScriptを読み込まないぶん表示も速いという利点があります。一方で、Escキーで閉じる・外側クリックで閉じる・リンク選択で閉じる・状態を読み上げソフトに正しく伝えるといった細かな制御はJavaScript版のほうが圧倒的にやりやすいです。仕事として納品するサイトでは、アクセシビリティを整えやすいJavaScript版を選ぶことが多いです。
A. 純CSS版なら、多くの場合ラベルとチェックボックスのfor・idが一致していないか、要素の並び順が「チェックボックス→ラベル→メニュー」になっていないことが原因です。JS版なら、外側クリックで閉じる処理でボタン自身を判定から除外できておらず、開いた瞬間に即閉じている可能性があります。まずは for と id、要素の順番、そしてクリック判定を見直してください。
A. 上下の線を回転させる前に、translateY(0)でいったん中央の高さへ戻せているかを確認してください。閉じた状態では上下の線が離れているので、そのまま回転させても×になりません。transform: translateY(0) rotate(45deg); のように、移動で中央に集めてから回転させるのが正解です。真ん中の線を background: transparent; で消し忘れていないかも合わせて確認しましょう。
A. 止められます。メニューを開いている間だけbodyにoverflow: hidden;を当てるのが定番の方法です。JavaScript版なら、開くときに document.body.classList.add("menu-open")、閉じるときに remove し、CSSで body.menu-open { overflow: hidden; } と書けば実現できます。閉じるときにクラスを外し忘れると、ページ全体がスクロールできなくなるので注意してください。
A. まずは①ボタンを<button>要素で作る、②aria-expandedを開閉に合わせて更新する、③Escキーで閉じられるようにするの3点を押さえれば、最低限のラインはクリアできます。余裕があれば、閉じたときにフォーカスをボタンへ戻す、prefers-reduced-motionで動きを控える、といった配慮も加えるとより丁寧です。完璧を目指しすぎて手が止まるより、まず3点から始めるのがおすすめです。
A. 表示しても問題はありませんが、PCでは横並びのナビゲーションを見せるのが一般的です。PCは画面が広く、メニューを隠す必要がないため、あえて隠すとユーザーが目的のページを探しづらくなることがあります。メディアクエリで、PCは横並びナビ、スマホはハンバーガー、と切り替えるのがおすすめです。ただしメニュー項目が非常に多いサイトでは、PCでもハンバーガーにまとめる設計もあり得ます。
A. 使っても構いませんが、CSSで3本線を作るほうが軽くて調整しやすいです。画像だと×への変形アニメーションが作りにくく、アイコンフォントは読み込みの手間が増えます。CSSの擬似要素で作れば、色・太さ・間隔・アニメーションまで自由自在です。この記事で紹介した span+擬似要素の方法をまず試してみてください。
A. タップできる範囲が小さすぎることが原因のことが多いです。ボタンの当たり判定は最低でも44px四方を確保するのが目安です。3本線自体は細くても、label や button の width/height と padding を広めに取り、指で押しやすい大きさにしましょう。見た目の線は細いままでも、押せる範囲だけ広げることができます。
ハンバーガーメニューは、一見むずかしそうに見えて、仕組みを分解すれば「開いている状態と閉じている状態を切り替えているだけ」というシンプルな部品です。この記事では、JavaScriptを使わない純CSSのチェックボックスハックから始めて、3本線が×に変わるアニメーション、スライドインやオーバーレイなどのバリエーション、JavaScript版、アクセシビリティ対応、レスポンシブ切り替え、そしてよくある失敗の直し方まで、コピペで動くコードとともに一通り解説しました。
まずは純CSS版を貼り付けて動かし、仕組みを理解したら、実務ではJavaScript版でEscキーや外側クリック、aria-expandedの更新まで整えていく——この順番で身につけるのがおすすめです。
・状態の切り替え:ハンバーガーメニューは「開・閉」2状態の切り替え。CSSなら:checked、JSなら.is-openクラスで実現する
・3本線と×:span1つ+::before/::afterで3本線を作り、中央へ集めてから回転させて×に変形させる
・仕上げの決め手:button要素・aria-expanded・キーボード操作・レスポンシブ切り替えまで整えて初めて実務レベルになる
ここまで読んで「コードの意味は分かったけれど、自分のサイトに組み込むと途端に動かなくなる」「HTML・CSS・JavaScriptのつながりを、体系立てて基礎から学び直したい」と感じた方もいるはずです。WithCodeなら、こうしたUI部品を自分の手で作りながら、現役エンジニアのサポートを受けてつまずきを最短で解消できます。独学で止まってしまいがちなポイントを、伴走型で乗り越えていけるのが特長です。

副業・フリーランスが主流になっている今こそ、自らのスキルで稼げる人材を目指してみませんか?
未経験でも心配することはありません。初級コースを受講される方の大多数はプログラミング未経験です。まずは無料カウンセリングで、悩みや不安をお聞かせください!
WithCodeでWeb制作を習得後、フリーランスエンジニアとして活動。HTML/CSS・JavaScript・WordPress案件を中心に年間20件以上の制作実績を持つ。「難しい技術をわかりやすく」をモットーに、初心者〜中級者向けの技術記事を執筆。副業・フリーランス独立を目指す方に向けた情報発信に注力している。
公式サイト より
今すぐ
無料カウンセリング
を予約!