ハンバーガーメニューが×に変わる動きをCSSで|aria-expandedで開け閉め、コピペ4種類

ハンバーガーメニューをCSSで×に変える。見本とコード

スマホのサイトの右上によくある、3本線のボタン。押すとメニューが開き、線が×に変わって「ここを押せば閉じる」と分かるようになります。この形の変化は、CSSの transform(回す・動かす指定)と、ほんの数行のJSで作れます。

使いどころ

スマホでメニューの項目が5つを超えるなら、3本線のボタンにまとめる。4つ以下なら、ボタンにせず並べて見せたほうが早い。×に変える動きは0.25秒ほどにとどめる。

今回は、基本の「3本線が×に変わる」、丸いボタンが回る形、「メニュー」の文字つき、押すとメニューが上から開く形の4種類を、コピペ用のコードつきで紹介します。線の太さや速さの目安、読み上げソフトやキーボードで使う人への配慮もまとめました。

トミー

連載「WEBの技術」の27回目です。
今回はハンバーガーメニュー。3本線がハンバーガーに見えるので、この名前で呼ばれています。

この記事の内容は、無料の道具でも試せます

擬似クラス チェッカー

:nth-child が「どこを選ぶか」が、図で分かる(登録なし)

道具で試してみる →

完成見本:×に変わる4種類

下の4つは、どれも本物の <button> にCSSで形を付けたものです。動きが分かるように、見本では開く・閉じるを自動でくり返しています。

HAMBURGER TO X1. 3本線が×に変わるいちばん基本の形。どのサイトにも合わせやすい
SPIN2. 丸いボタンが回りながら×にボタンを目立たせたい、写真の上に置きたいとき
WITH LABEL3. 「メニュー」の文字つき3本線だけでは伝わりにくい、幅広い年代が見るサイト
WITH MENU4. メニューが上から開くスマホのヘッダーに、そのまま組みこむとき

※ 見本は読み上げソフトでは読まれないようにし、キーボードでも止まらないようにしています。端末で動きを減らす設定にしている場合は、切りかわりが止まります。

コピペ用のコード

CSSは、サイトのCSSファイルや、WordPressならテーマの「追加CSS」などに入れます。クラス名の頭の kd-hb は、サイトのほかのデザインと名前がぶつからないようにするための目印です。1のCSSは2〜4でも使います。JSは4種類とも共通で、最後にまとめて載せています。

1. 3本線が×に変わる

3本の線を <span> で並べ、開いたときに上と下の線を真ん中へ寄せてから45度ずつ回し、真ん中の線を消しています。どちらに変わるかは、ボタンの aria-expanded(メニューが開いているかどうかを伝える属性)で決めます。下のCSSは、2〜4でも共通で使う「ボタンと3本線」の指定です。

HTML
<button class="kd-hb" type="button" aria-expanded="false" aria-controls="kd-hb-menu" aria-label="メニュー">
  <span class="kd-hb-icon" aria-hidden="true"><span></span><span></span><span></span></span>
</button>
CSS
/* 1〜4で共通:ボタンと3本線 */
.kd-hb {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 4px;
  width: 48px;
  height: 48px;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: transparent;
  color: #10283A;
  cursor: pointer;
}
.kd-hb-icon {
  position: relative;
  display: block;
  width: 24px;
  height: 18px;
}
/* 線は枠線で描く(強制カラーモードでも消えない) */
.kd-hb-icon span {
  position: absolute;
  left: 0;
  width: 100%;
  height: 0;
  border-top: 2px solid currentColor;
  transition: transform .25s ease, opacity .2s ease;
}
.kd-hb-icon span:nth-child(1) { top: 0; }
.kd-hb-icon span:nth-child(2) { top: 8px; }
.kd-hb-icon span:nth-child(3) { top: 16px; }
/* 開いたとき:上と下を真ん中に寄せて45度ずつ回し、真ん中を消す */
.kd-hb[aria-expanded="true"] .kd-hb-icon span:nth-child(1) {
  transform: translateY(8px) rotate(45deg);
}
.kd-hb[aria-expanded="true"] .kd-hb-icon span:nth-child(2) {
  opacity: 0;
}
.kd-hb[aria-expanded="true"] .kd-hb-icon span:nth-child(3) {
  transform: translateY(-8px) rotate(-45deg);
}
.kd-hb:focus-visible {
  outline: 3px solid #0284C7;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
  .kd-hb-icon span {
    transition: none;
  }
}

2. 丸いボタンが回りながら×に

1のボタンに、濃い青の丸い背景と白い線を付けました。開くときは、3本線を入れた箱(kd-hb-icon)ごと180度回します。線が×に変わる動きと、回る動きが同時に起きるので、1より少しにぎやかに見えます。

HTML
<button class="kd-hb kd-hb-spin" type="button" aria-expanded="false" aria-controls="kd-hb-menu" aria-label="メニュー">
  <span class="kd-hb-icon" aria-hidden="true"><span></span><span></span><span></span></span>
</button>
CSS
.kd-hb-spin {
  border-radius: 50%;
  background: #0369A1;
  color: #FFFFFF;
}
.kd-hb-spin .kd-hb-icon {
  transition: transform .4s ease;
}
.kd-hb-spin[aria-expanded="true"] .kd-hb-icon {
  transform: rotate(180deg);
}
@media (prefers-reduced-motion: reduce) {
  .kd-hb-spin .kd-hb-icon {
    transition: none;
  }
}

3. 「メニュー」の文字つき

3本線の下に「メニュー」の文字を入れました。文字があるので aria-label(読み上げ用の名前)は付けず、見えている文字をそのままボタンの名前にしています。文字は開いても「メニュー」のまま変えません。開いているかどうかは aria-expanded が伝えるので、名前まで変えると二重になるためです。

HTML
<button class="kd-hb kd-hb-text" type="button" aria-expanded="false" aria-controls="kd-hb-menu">
  <span class="kd-hb-icon" aria-hidden="true"><span></span><span></span><span></span></span>
  <span class="kd-hb-label">メニュー</span>
</button>
CSS
.kd-hb-text {
  width: 64px;
  height: 56px;
  gap: 6px;
}
.kd-hb-label {
  font-size: 11px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: .04em;
}

4. メニューが上から開く

1のボタンをヘッダーに置き、押すとすぐ下にメニューが出るようにしました。JSが aria-controls に書いたIDのメニューに is-open を付けたり外したりします。閉じているあいだは visibility: hidden(見えなくして、キーボードでも選べなくする指定)にしているので、見えないリンクにTabキーで止まることはありません。

HTML
<header class="kd-hb-header">
  <a class="kd-hb-logo" href="/">サイト名</a>
  <button class="kd-hb" type="button" aria-expanded="false" aria-controls="kd-hb-menu" aria-label="メニュー">
    <span class="kd-hb-icon" aria-hidden="true"><span></span><span></span><span></span></span>
  </button>
  <nav id="kd-hb-menu" class="kd-hb-nav" aria-label="メインメニュー">
    <ul>
      <li><a href="/service/">サービス</a></li>
      <li><a href="/works/">制作実績</a></li>
      <li><a href="/blog/">ブログ</a></li>
      <li><a href="/contact/">お問い合わせ</a></li>
    </ul>
  </nav>
</header>
CSS
.kd-hb-header {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 8px 8px 20px;
  background: #FFFFFF;
  border-bottom: 1px solid #E3E9EE;
}
.kd-hb-logo {
  font-weight: 700;
  color: #10283A;
  text-decoration: none;
}
/* 閉じているとき:見えない・選べない */
.kd-hb-nav {
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  z-index: 10;
  background: #FFFFFF;
  box-shadow: 0 12px 24px rgba(16, 40, 58, .12);
  visibility: hidden;
  opacity: 0;
  transform: translateY(-8px);
  transition: opacity .25s ease, transform .25s ease, visibility 0s .25s;
}
.kd-hb-nav.is-open {
  visibility: visible;
  opacity: 1;
  transform: none;
  transition-delay: 0s;
}
.kd-hb-nav ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
.kd-hb-nav a {
  display: block;
  padding: 14px 20px;
  border-top: 1px solid #E3E9EE;
  color: #10283A;
  text-decoration: none;
}
@media (prefers-reduced-motion: reduce) {
  .kd-hb-nav {
    transform: none;
    transition: none;
  }
}

※ コードの上の見本は、比べやすいように開いた形で止めています。

4種類で共通のJS

ボタンを押すたびに aria-expanded を true と false で切りかえ、aria-controls に書いたIDのメニューがあれば、is-open を付けたり外したりします。開いているときに Esc キーを押すと閉じて、ボタンに戻ります。</body> の直前などに入れてください。

JS
document.querySelectorAll('.kd-hb').forEach(function (btn) {
  var menu = document.getElementById(btn.getAttribute('aria-controls'));
  function setOpen(open) {
    btn.setAttribute('aria-expanded', open);
    if (menu) menu.classList.toggle('is-open', open);
  }
  btn.addEventListener('click', function () {
    setOpen(btn.getAttribute('aria-expanded') !== 'true');
  });
  // Escキーで閉じて、ボタンに戻る
  document.addEventListener('keydown', function (e) {
    if (e.key === 'Escape' && btn.getAttribute('aria-expanded') === 'true') {
      setOpen(false);
      btn.focus();
    }
  });
});

仕組み:aria-expanded を「スイッチ」にする

状態はボタンの属性に1つだけ持たせる

よく見かけるのは、JSでボタンに is-active のようなクラスを付けて、CSSでそのクラスを見て形を変える書き方です。今回は、クラスの代わりに aria-expanded を使っています。MDNでは、開け閉めするものを操作するボタンには、aria-controls に相手のIDを、aria-expanded に今の状態を付けるよう説明されています。

この属性は、読み上げソフトに「メニュー、ボタン、折りたたまれています」のように状態を伝えるためのものです。CSSの [aria-expanded="true"](属性の値で選ぶ書き方)で見た目も切りかえれば、「見た目は×なのに、読み上げでは閉じたまま」というずれが起きません。JSは属性を書きかえるだけで済みます。

×は「寄せてから回す」

3本の線は、上から0px、8px、16pxの位置に置いています。開いたときは、上の線を8px下げて45度、下の線を8px上げてマイナス45度回します。2本とも真ん中の高さで交差するので、×になります。真ん中の線は opacity: 0(透明にする指定)で消しています。

transform に translateY(8px) rotate(45deg) と2つ並べると、左から順に「下げる」「回す」が行われます。順番を逆にすると、回したあとの向きのまま下がるため、線が斜めにずれます。

メニューは visibility で隠す

4のメニューは、閉じているときに opacity: 0 で透明にするだけでなく、visibility: hidden も付けています。MDNによると、visibility: hidden の要素はTabキーで選べなくなり、読み上げの対象からも外れます。透明にしただけだと、見えないリンクにキーボードの選択が止まってしまいます。

ただし visibility は、アニメーションでなめらかには変わらず、途中で切りかわります。そこで閉じるときは visibility 0s .25s(0.25秒待ってから切りかえる)として、透明になり終わってから隠しています。

数値を変えるとどうなるか

線の太さ

1のボタンの、線(border-top)の太さだけを変えました。

1px:細くて上品だが見えにくい
2px:目安の太さ
3px:力強い

1pxは上品に見えますが、スマホの小さな画面や、明るい屋外では見えにくくなります。3pxは力強く、文字の太いサイトに合います。迷ったら2pxにして、ロゴや見出しの文字の太さに合わせて調整します。

切りかわりの速さ

transition(変わるまでの時間)だけを変えて、自動でくり返しています。

0.1秒:ぱっと変わる
0.25秒:目安の速さ
0.6秒:ゆっくり

0.1秒では動きがほとんど見えず、0.6秒では、メニューを開くたびに少し待たされる感じがします。ボタンは何度も押すものなので、0.2〜0.3秒ほどが目安です。今回の見本は0.25秒にしています。

押せる大きさ

点線が、押せる範囲(ボタンの大きさ)です。線の長さは同じ24pxです。

32px:見た目は同じでも押しにくい
48px:目安の大きさ

見た目が同じでも、ボタンが小さいと指で押しにくくなります。W3Cのアクセシビリティの基準(WCAG 2.2)では、押す部分は24×24ピクセル以上が目安です。今回の見本は、線のまわりに余白をとって48×48pxにし、目安より広めにしています。

変えるところ小さく・短くすると大きく・長くすると
線の太さ(1の 2px)上品だが、見えにくい力強いが、重たく見える
線の間かく(1の 8px)線がくっついて見え、×も小さくなるすかすかして、3本線に見えにくい
速さ(1の .25s)変化に気づきにくい開くたびに待たされる
ボタンの大きさ(1の 48px)押しにくい画面の上の部分をふさぐ

スマホと、見る人への配慮

アイコンだけのボタンには名前を付ける

3本線だけのボタンには文字がないので、そのままでは読み上げソフトが「ボタン」としか読めません。1・2・4では aria-label="メニュー" で名前を付けています。3のように文字が見えているときは、その文字が名前になるので、aria-label は付けません。

開いても、名前は「メニュー」のまま

×に変わったからといって、名前を「閉じる」に書きかえる必要はありません。開いているかどうかは aria-expanded が伝えます。名前と状態の両方を変えると、「閉じる、ボタン、展開されています」のように、意味が重なって伝わります。

Esc キーで閉じて、ボタンに戻る

W3Cのアクセシビリティの例では、メニューが開いているときに Esc キーを押すと閉じて、メニューを開いたボタンに戻るようになっています。今回のJSも同じ動きにしました。キーボードで操作している人が、開いたメニューから抜けられなくなるのを防ぎます。

強制カラーモードでも線が消えないように

Windowsの「コントラスト テーマ」のような強制カラーモードでは、背景の色がブラウザの決めた色に置きかえられます。線を background で描くと、まわりと同じ色になって消えることがあります。そこで今回は、線を border-top(上の枠線)で描いています。

動きを減らす設定では、ぱっと切りかえる

端末で動きを減らす設定にしている人には、prefers-reduced-motion で切りかわりの時間をなくし、×とメニューがすぐに出るようにしています。形は変わるので、開いているかどうかは分かります。

やりすぎ注意

線が1本ずつ時間差で回る、ボタンが跳ねる、といった演出は、見本としては目を引きますが、毎回メニューを開くたびに見せられるものです。ボタンの動きは「開いた・閉じた」が分かれば十分なので、短く、1回で終わる動きにしています。

トミー

ハンバーガーメニューは、作り手が思っているほど「押せば開く」と伝わっていないことがあります。
迷ったら、3本線の下に「メニュー」と書いておくのがいちばん確実です。

AIに頼むなら

ハンバーガーメニューは、Claudeなどの生成AIに頼むとすぐ作ってもらえます。そのときは、形だけでなく「状態の持たせ方」と「閉じ方」まで伝えると、手直しが少なく済みます。

スマホ用のハンバーガーメニューを作って。button に aria-expanded と aria-controls を付け、
押すと3本線が0.25秒で×に変わり、ヘッダーの下にメニューが開くようにして。
閉じているメニューは visibility: hidden、Escキーで閉じてボタンに戻し、prefers-reduced-motion では動かさないで。

AIが出したコードは、そのまま使う前に、次の3つを確かめます。

  • ボタンが div や span になっていないか。<div onclick> で作られていると、キーボードで選べず、読み上げでもボタンと伝わりません。Tabキーで選んで、Enterキーで開けるかを試します
  • 閉じたメニューにTabキーで止まらないか。opacity: 0 や transform だけで隠していると、見えないリンクを選べてしまいます
  • aria-expanded が切りかわっているか。見た目はクラスで変わっていても、属性が false のままのことがあります。ブラウザの開発者ツールで、押したときに値が変わるかを見ます

KAKERUならこう使う/使わない

最後に、作り手としての判断です。

使う場面

  • スマホで、メニューの項目が5つを超えるとき。サービス・実績・会社案内・ブログ・お問い合わせ・採用、と並ぶ会社のサイトでは、ボタンにまとめないと画面の上がメニューで埋まります
  • 幅広い年代が見るサイトでは、3の文字つき。3本線の意味を知らない人にも、文字があれば伝わります
  • 写真が全面に広がるサイトでは、2の丸いボタン。背景の色に左右されず、どこにボタンがあるかが分かります

使わない場面

  • PCの画面幅。PCでは、メニューを並べて見せる場所が十分にあります。ボタンにしまうと、どんなページがあるかが一目で分からなくなります
  • メニューが4つ以下のとき。スマホでも横に並べられる数なら、ボタンを押してもらう手間をかけずに見せます
  • お問い合わせや予約など、いちばん押してほしい項目。メニューの中にしまわず、ボタンの横や画面の下に別に出しておきます
トミー

ハンバーガーメニューは「しまう」ための部品です。
しまっていいものと、出しておくべきものを分けてから使うようにしています。

まとめ

  • 開いているかどうかは、ボタンの aria-expanded に持たせる。CSSは [aria-expanded=”true”] で形を変える
  • ×は、上と下の線を真ん中に寄せてから45度ずつ回し、真ん中の線を消す
  • 閉じたメニューは visibility: hidden で、見えなくするだけでなく選べなくする
  • アイコンだけのボタンには aria-label。Esc キーで閉じてボタンに戻す
  • 線は2px前後、ボタンは48px前後、切りかわりは0.25秒ほど

まずは1のボタンをコピーして、自分のサイトの色に合わせてみてください。color を変えるだけで、線の色がまとめて変わります。

参考にした資料

いずれも2026年10月27日に確認しました。ブラウザの対応状況は変わることがあります。最新の情報は各ページでご確認ください。

ふだんの制作の話や、作ったものの裏側は Instagram と X でも出しています。

関連記事

ヘッダーを上に固定するCSS。position: stickyが止まらない原因と、使う・使わないの判断
開け閉めするアコーディオンをCSSだけで コピペで使える5種類と、使う・使わないの判断
ページの一番上に戻るボタン。置く・置かないの判断と、押しやすい大きさ
チェックボックスの見た目をCSSで変える。見本とコード
ハンバーガーメニューをCSSで×に変える。見本とコード

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

CAPTCHA