【脱・車輪の再発明】dialogタグで面倒が減る!見直したいアクセシブルなハンバーガーメニューの作り方

スマートフォン向けWebサイト制作において、もはや定番中の定番となっている「ハンバーガーメニュー(ドロワーメニュー)」。

ボタンをタップすると横からスルスルとナビゲーションがスライドして開き、背景が薄暗くなってメニューに集中させるUIは、誰もが一度は実装した経験があるはずです。

しかし、この一見シンプルなハンバーガーメニュー、「本当にアクセシブルで壊れない設計」を自前でゼロから組もうとすると、以下のような途方もない手間とバグに直面しませんか?

  • 「メニューが開いている間、キーボードのTabキーを押すと背面のリンクまでフォーカスが移動してしまう(フォーカストラップの実装が大変)」
  • 「Escキーを押してもメニューが閉じてくれない」
  • 「メニューの背後でコンテンツがスクロールしてしまい、overflow: hidden やスクロール固定のJavaScriptハックが必要になる」
  • 「最前面に表示するためだけに z-index: 9999 のような無理な指定を繰り返してしまう」
  • 「スクリーンリーダーに対して開閉状態を正しく伝えるための aria-* 属性の設定が複雑」

これらの長年フロントエンド開発者を悩ませてきた「ハンバーガーメニューの面倒くさい課題」を一発で解決してくれる救世主が、HTML標準の<dialog> タグです。

<dialog> はモーダルウィンドウ専用だと思われがちですが、実は画面端からスライドインするドロワーナビゲーションの実装にこそ真価を発揮します。

本記事では、従来の自作メニューが抱えていたアクセシビリティの落とし穴から、<dialog> タグを使う絶大なメリット、CSSアニメーション(スライドイン)を効かせる最新の実装コード、実務での注意点まで、徹底解説します!


1. 従来の自作ハンバーガーメニューが抱える「5大ストレス」

なぜ従来の <div class="drawer"> とJavaScriptで作るハンバーガーメニューは、これほど壊れやすく保守が辛かったのでしょうか?その理由を整理してみましょう。

① フォーカストラップ(Tabキー制御)の自作が辛い

キーボード操作を行うユーザーやスクリーンリーダー利用者がメニューを開いたとき、Tabキーによるフォーカス移動は「メニュー内の閉じるボタンやリンクの間だけ」をループ(トラップ)しなければなりません。これを自作する場合、フォーカス可能な要素を配列で取得し、最後の要素でTabが押されたら先頭に戻すといった複雑なイベントリスナーを書く必要がありました。

② Escキーで閉じる処理の追加

キーボードユーザーにとって「モーダルやドロワーを閉じる標準的な操作」は Escape キーです。自作メニューでは keydown イベントを監視し、明示的に閉じる関数を実行させなければなりませんでした。

③ 背面スクロールの固定(スクロール連動問題)

スマートフォンでメニューを開いている最中、画面を指で上下にスワイプすると、背後にある本文ページまで一緒にスクロールしてしまう現象です。これを防ぐために body { overflow: hidden; position: fixed; } などを当てるハックが多用され、スクロール位置がトップに戻ってしまう等の不快なバグを引き起こしていました。

④ z-indexのインフレとスタッキングコンテキストの罠

「メニューを何としても最前面に出したい」という理由で z-index: 99999; を指定した結果、ヘッダーの固定要素やモーダル、チャットウィジェットなどと衝突し、管理不能なCSSになってしまう現場が後を絶ちません。

⑤ スクリーンリーダーへの配慮(アクセシビリティ)

メニューが開いている間、背後にあるコンテンツは支援技術から無視(不活性化 / inert)されなければなりません。これを手動で行うには、メインコンテンツ全体に aria-hidden="true"inert 属性を付け替える必要がありました。


2. <dialog> タグを採用する圧倒的なメリット

HTML標準の <dialog> タグを使い、JavaScriptの dialog.showModal() でメニューを開くだけで、上記の面倒な問題のすべてが「ブラウザの標準機能」として自動的に解決されます。

実装課題 従来の自作 <div> メニュー <dialog> タグ(showModal)
フォーカストラップ 自前でJSをゴリゴリ書く必要がある ブラウザが完全自動でメニュー内に閉じる
Escキーで閉じる keydownイベントの監視が必要 標準でEscキーを押せば自動で閉じる
背面コンテンツの不活性化 aria-hiddeninert の付与が必要 背後の要素が自動的に完全操作不能(inert)になる
最前面への描画 z-index の数値バトルが発生 最上位レイヤー(Top Layer)に描画され、z-index不要
背景のオーバーレイ(暗幕) 専用の <div class="overlay"> を配置 ::backdrop 擬似要素でCSS 1行で指定可能

特に重要なのが**「Top Layer(最上位レイヤー)」**の存在です。どれだけ親要素のCSSで overflow: hiddentransform が設定されていても、showModal() で開かれた <dialog> はあらゆる要素のスタッキングコンテキストを突き抜けて画面の一番手前に描画されます。もう z-index に頭を悩ませる必要は一切ありません。


3. 【実践コード】dialogタグで作るハンバーガーメニュー完全版

それでは、実際のWebサイトにそのまま組み込める、綺麗に右からスライドインするハンバーガーメニューのコードを見てみましょう。

HTMLサンプルコード

<!-- 開くためのボタン -->
<button type="button" id="open-menu" class="hamburger-btn" aria-label="メニューを開く">
  <span class="bar"></span>
  <span class="bar"></span>
  <span class="bar"></span>
</button>

<!-- dialogメニュー本体 -->
<dialog id="drawer-menu" class="drawer" aria-label="メインナビゲーション">
  <div class="drawer-inner">
    <div class="drawer-header">
      <!-- 閉じるボタン -->
      <button type="button" id="close-menu" class="close-btn" aria-label="メニューを閉じる">
        ✕
      </button>
    </div>
    <nav class="drawer-nav">
      <ul>
        <li><a href="#home">ホーム</a></li>
        <li><a href="#about">会社概要</a></li>
        <li><a href="#services">サービス</a></li>
        <li><a href="#contact">お問い合わせ</a></li>
      </ul>
    </nav>
  </div>
</dialog>

CSSサンプルコード(最新のアニメーション対応)

モダンブラウザでサポートされている @starting-styletransition-behavior: allow-discrete を活用し、display: none(非表示)状態から滑らかにスライドイン・スライドアウトするアニメーションを実現します。

/* ドロワーメニューの基本スタイル */
.drawer {
  position: fixed;
  inset: 0 0 0 auto; /* 右端にぴったり固定 */
  width: min(80vw, 360px); /* 画面幅80%または最大360px */
  height: 100dvh;
  margin: 0;
  padding: 24px;
  background-color: #ffffff;
  border: none;
  box-shadow: -4px 0 20px rgba(0, 0, 0, 0.15);

  /* スライドインのための初期設定 */
  transform: translateX(100%);
  transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1),
              display 0.3s allow-discrete,
              overlay 0.3s allow-discrete;
}

/* 開いた状態([open]属性が付与された時) */
.drawer[open] {
  transform: translateX(0);
}

/* 表示開始時のスタイル(開く瞬間のアニメーション定義) */
@starting-style {
  .drawer[open] {
    transform: translateX(100%);
  }
}

/* 背景の暗幕(::backdrop) */
.drawer::backdrop {
  background-color: rgba(0, 0, 0, 0);
  backdrop-filter: blur(0px);
  transition: background-color 0.3s ease,
              backdrop-filter 0.3s ease,
              display 0.3s allow-discrete,
              overlay 0.3s allow-discrete;
}

.drawer[open]::backdrop {
  background-color: rgba(0, 0, 0, 0.5);
  backdrop-filter: blur(2px);
}

@starting-style {
  .drawer[open]::backdrop {
    background-color: rgba(0, 0, 0, 0);
    backdrop-filter: blur(0px);
  }
}

/* ドロワー内部のUIデザイン */
.drawer-inner {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.drawer-header {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 24px;
}

.close-btn {
  background: transparent;
  border: none;
  font-size: 24px;
  cursor: pointer;
  padding: 8px;
}

.drawer-nav ul {
  list-style: none;
  padding: 0;
  margin: 0;
}

.drawer-nav li {
  border-bottom: 1px solid #f0f0f0;
}

.drawer-nav a {
  display: block;
  padding: 16px 8px;
  color: #333333;
  text-decoration: none;
  font-weight: bold;
}

JavaScriptサンプルコード(わずか十数行!)

JavaScriptでやるべきことは、dialog.showModal()dialog.close() を呼び出すだけです。

const drawer = document.getElementById('drawer-menu');
const openBtn = document.getElementById('open-menu');
const closeBtn = document.getElementById('close-menu');

// メニューを開く(※show()ではなくshowModal()を使うのが超重要!)
openBtn.addEventListener('click', () => {
  drawer.showModal();
});

// 閉じるボタンで閉じる
closeBtn.addEventListener('click', () => {
  drawer.close();
});

// 背景(backdrop)をクリックした時に閉じる処理
drawer.addEventListener('click', (event) => {
  if (event.target === drawer) {
    drawer.close();
  }
});

// メニュー内のリンクをクリックした時にも閉じる
drawer.querySelectorAll('a').forEach((link) => {
  link.addEventListener('click', () => {
    drawer.close();
  });
});

4. ここに注意!実装時に押さえるべき重要ポイント

<dialog> を使ったハンバーガーメニューで失敗しないための実務上の注意点をまとめました。

① show() ではなく必ず showModal() を使うこと

<dialog> には開くためのメソッドが2つあります。

  • show():通常のインライン要素として開く。Top Layerに配置されず、フォーカストラップや背景の無効化も行われません。
  • showModal()(推奨):モーダルとして開く。Top Layerに配置され、フォーカストラップ、背面のinert化、Escキー対応、::backdrop のすべてが自動で有効化されます。

ハンバーガーメニューの実装では、必ず showModal() を選択してください。

② 背景クリック判定の仕組み

<dialog> の背景(::backdrop)をクリックしたときにメニューを閉じたい場合、JavaScriptで event.target === drawer かどうかを判定します。これは、<dialog> の余白や背景エリアをクリックした際、イベントのターゲットがダイアログ本体自身になるという特性を利用したシンプルで美しいテクニックです。

③ 閉じた後の「フォーカス復元」も標準装備

自作メニューでは「メニューを閉じた後、開くボタンにフォーカスを戻す」という処理を手動で書く必要がありましたが、<dialog> ではメニューを閉じると「メニューを開く直前にフォーカスしていた要素(開くボタン)」に自動的にフォーカスが戻ります。これにより、キーボード操作の文脈が途切れることがありません。


5. まとめ:車輪の再発明をやめて、Web標準の恩恵を受けよう

長年当たり前のように <div> と複雑なJavaScriptライブラリで組まれてきたハンバーガーメニューですが、モダンブラウザが出揃った今、もはや自前で苦労して実装する必要はありません。

  1. <dialog>showModal() を使えば、フォーカストラップやEscキー対応が完全自動化される。
  2. Top Layerのおかげで z-index の管理が不要になり、CSSが劇的にスッキリする。
  3. @starting-styleallow-discrete を組み合わせることで、CSSだけで滑らかなスライドイン・フェードインが完結する。
  4. コード量が従来の数分の一になり、バグの温床を根本から排除できる。

アクセシビリティの向上とコードの軽量化を同時に達成できる <dialog> によるハンバーガーメニュー。次回のWebサイト制作やリニューアル案件で、ぜひ見直してみてはいかがでしょうか!

関連記事
👉 【完全保存版】WordPressサーバー移行の全手順!現役エンジニアが失敗しない手順と注意点を徹底解説

(Visited 1 times, 1 visits today)
タイトルとURLをコピーしました