こなさんみんばんわ。
さて、(自分んとこのことなのに)いつごろ変更したのかもうすっかり忘れてしまったのですが、長い間ロゴしか置いてなかったサイトのバナーに、グローバル・ナビゲーション的なものを配置しました。このナビゲーションは PC などの広い画面で閲覧する時は普通の横並びのメニューですが、狭い画面ではボタンによる開閉式、いわゆるハンバーガーメニューにスイッチするという、昨今ではよくある形式のものです。
数年前だとこういったメニューを実装するには JavaScript が不可欠だった訳ですが、HTML と CSS の進化により、この記事を書いている2026年現在では JavaScript をひとつも書くことなく1実装できるようになっています。
ということで、本日は画面幅に応じてハンバーガーメニューと横並びが切り替わるナビゲーション(以下、この記事では便宜上レスポンシブ・メニューと称します)を HTML と CSS だけで実装する方法を、重要なポイントに絞って解説していきます。ちなみに先に書いておきますが input:checked と兄弟セレクタを使う方法ではないです。
No JS レスポンシブ・メニューの動作サンプル
このサイトのヘッダーがそのままサンプルです! といきたかったのですが、数ヶ月後・数年後に違う実装に変更してて、それ以降に読んだ人が「あれ、書いてある内容と全然違う…」となる可能性がなきにしもあらずなので、別個で動作サンプルを作っておきました。アニメーションなども加えたので(2026年6月時点での)ここのものとはちょっと違いますが、骨格は一緒です。
See the Pen No JS Responsive Menu (without Closing Animation) by Jeffrey Francesco (@jforg) on CodePen.
画面幅672ピクセル以上を確保できるデバイスであれば2、左ペインの表示 / 非表示を切り替えることでメニューの状態も切り替わるはずです。また、構成ファイルの中に JavaScript が一切含まれていない(HTML ソースの中にも)ことも確認できるかと思います。
過去のレスポンシブ・メニューがどうやって実装されてきたか
このようなレスポンシブ・メニューを実装するにあたって、過去によく行われていたのは、軽く調べてみた限りだと、王道はおおむね次のような感じではないかと思われます:
<div>など既存の要素に WAI-ARIA の属性を付与してポップアップするメニュー部分をマークアップ- ポップアップの状態に応じた
class付与やキーボード操作時の処理など諸々を JavaScript で実装 - ハンバーガーと横並びのメニューを2つ準備しておき、メディアクエリーで画面幅に応じて出し入れ
ですが、こういったメニュー、特にハンバーガーメニューをアクセシビリティまで含めてまともに実装しようとすると、あまりにも考慮するべき事項が多すぎてけっこう骨の折れる作業になることは、ちゃんとした制作をされてきた経験者の方であれば、おそらくご存知ではないかと思います。
あまり深く考えずにやってこられた方がどうだかは知らないです…
No JS でレスポンシブ・メニューを実装するための3つの Web 標準
それでは2026年となった今、どのような手法でレスポンシブ・メニューを実装するのがいいのでしょうか? 以下ではそのポイントとなる3つの Web 標準について、順に紹介と解説をしていきます。
HTML の <dialog> 要素
経験上、ハンバーガーメニューはおおむねモーダル・ダイアログ的な手法…つまりメニューが開いた状態では背面のコンテンツを操作できない…で実装されてきた方が多いと思いますが、既存の要素でモーダルを実装しようとする場合、例えば Dialog (Modal) Pattern | APG | WAI | W3C で挙げられているようなキーボード操作などへの対応を始め、背景の不活性化や外側クリックで閉じる機能などなど、数多くの考慮事項がありました。
ですが、今はもう <dialog> 要素を使えば、これら一連のロジックをほぼすべて面倒見てくれます。マークアップに特筆すべき点はなく、せいぜい aria-labelledby もしくは aria-label で名前付けをするくらいでしょう:
<dialog aria-label="メニュー">
<!-- メニュー部分(省略) -->
</dialog>
これについてはすでに実務で使用されている方も多いでしょうし、多くは説明不要かと思いますので、次にいきます。
Web API の Invoker Commands
<dialog> 要素が使えるようにはなりましたが、それを開くためには開くための、閉じるためには閉じるための <button> 要素を用意し、それぞれの click イベントに対して <dialog> の showModal() や close() メソッドを呼び出すためのイベントリスナーを登録する必要がありました。つまり依然として JavaScript が必要でした…ええ、少し前までは。
実は最近(昨年〜)では HTML を書くだけで、<button> に <dialog> を開いたり閉じたりする動作を追加できるようになっています。それがこの Invoker Commands API です。
具体的には <button> タグの commandfor 属性に操作したい <dialog> の id 値を与えて関連付けを示し、command 属性で <dialog> に対して行う操作内容を宣言します。実例として、先の動作サンプルのコードからポイントのみを抜き出した HTML コードを以下に示します:
<!-- ダイアログをモーダルとして開く show-modal コマンドを付与 -->
<button class="banner-button" command="show-modal" commandfor="gNavDialog" type="button">
<svg><!-- 略 --></svg>
</button>
<!-- 各ボタンの commandfor 属性でこの ID 値 (gNavDialog) を指定することで、操作対象であることを関連付けている -->
<dialog class="nav" closedby="any" id="gNavDialog">
<nav class="nav-content">
<!-- ダイアログを閉じる close コマンドを付与 -->
<button class="nav-button" command="close" commandfor="gNavDialog" type="button">
<svg><!-- 略 --></svg>
</button>
<ul><!-- 略 --></ul>
</nav>
</dialog>
すでにあるポップオーバー API と同じような感じですが、この command 属性に指定できる値には show-popover などポップオーバー関連のコマンドなども含まれており、いわばポップオーバー API をより汎用的にした API という位置付けです。今回はこの辺の説明は省略させていただきますので、詳しくお知りになりたい方は MDN の該当ページなどをご覧ください。
あと出てきたついでにまとめて説明しておきますと、closedby 属性というのは <dialog> 要素をどのような操作で閉じられるようにするかを指定できるものです。値は any, closerequest, none のいずれかで、今回のように any を指定すると背景をクリックやタップすることで閉じる、いわゆる light dismiss(簡単に閉じる)が有効になります。
CSS の display: contents プロパティ値
過去のレスポンシブ・メニューはどうやって実装されてきたかで触れた実装方法の3つ目にハンバーガーと横並びのメニューを2つ準備しておき、メディアクエリーで画面幅に応じて出し入れ
というのを書きましたが、いくら出し分けのためとはいえ、ほぼ同じ内容のものを同じ場所もしくは近い場所に2つ書くという手法は、僕はあんまり好きじゃないのですよね…
という訳で、2つもメニューを書くことなく、ワンソースで切り替えを実現したいのですが、これを実現するのに便利なのが CSS の display プロパティに指定できる contents という値。これを指定した要素は自身をレンダリングするための特定のボックスを生成しなくなり、その場所には自身の子要素が、元からその子要素しか存在しなかったかのように表示されます。
動作サンプルでいうと以下の部分が該当するコードで、ビューポートの幅と高さが十分に確保できる状態の場合に .nav クラスが与えられた <dialog> 要素を display: contents にすることで、子要素である <nav> 要素が表に露出するようにしています。他のルールで指定している <dialog> のスタイル(padding や inline-size など)の値がレイアウトに一切影響していないことに、特に注目していただければと思います。
@media (width >= 40em) and (height > 480px) {
.nav {
display: contents;
}
.banner-button {
display: none;
}
/*
補足: ここは「ダイアログが開いてない時」に限定することで、
万一ダイアログが開いた状態でビューポートの大きさが変更になった場合でも
「閉じるボタンがなくてコンテンツに戻れない」状態が発生しないようにしている
*/
dialog:not([open]) {
.nav-button {
display: none;
}
/* 以下、省略 */
}
}
なお、かつては多くのブラウザに display: contents を指定した要素がアクセシビリティ・ツリーから除外されてしまうという不具合が存在したのですが、確認した限り、現在ではどのブラウザでも修正されているようなので、今はあまり気にする必要はないように思います。ていうかこの場合、開閉できないのに dialog ロールが付いたままだと逆に混乱をきたす気がするので、できれば積極的に除外してほしかったりもしますが…
いくつかの問題
以上、ここまでに紹介した3つの Web 標準を使うことで、基本的には JavaScript を使わずに(さらには2つもメニュー部分を書くことなく)レスポンシブ・メニューを実装することが可能になりました!!
…と断言したいところなのですが、実は現時点(2026年6月14日現在)ではこの方法にもいくつか問題点があり、まだ実際には JavaScript の追加や若干の工夫が必要になります。
少し古いバージョンの Web ブラウザでは動作しない
特に Invoker Commands API については昨年 Baseline (2025) となったばかりであり、少し古いバージョンのブラウザでは動作しません。Can I Use… のデータで見るとグローバルでまだ78.4%ほどなので、2割くらいのブラウザで動作しない計算ですね。また closedby 属性については Safari では Technology Preview 242 にてようやく実装されたばかりであり、リリース版ではまだ機能しません。
なので、これらに関しては機能検出を行い、未サポートの場合は JavaScript によるフォールバックを適用する必要があるでしょう。具体的な方法については、自力でやるのであれば以前僕が Zenn に書いた記事があるので、そちらを参考にしていただければと思います。とりあえずモーダルに適用するだけなら、これでも十分事足りるはずです:
- button 要素の command & commandfor 属性のフォールバックを自力で書いてみた(ポップオーバー関連には目を背けつつ)
- dialog 要素の closedby 属性のフォールバックを自力で書いてみた(非モーダルのダイアログからは目を反らしつつ)
ちなみに Polyfill ライブラリもあるようなので紹介しておきますが、後者はどうも Safari 最新版で動作してないっぽいです(テストページで確認しました)。
- keithamus/invokers-polyfill
- tak-dcxi/dialog-closedby-polyfill: A polyfill for the HTMLDialogElement closedBy attribute, providing control over how modal dialogs can be dismissed.
ビューポートのサイズが変わる時に意図しない挙動をする(ことがある)
これはバグとかではないので、単に実装側で注意しておけばいいだけの話なのですが、CSS Transition を使ってダイアログを閉じる際にアニメーションを付けていると、ビューポートのサイズが変わって横並びからハンバーガーメニューへの変化が生じる際に、本来であれば画面に表示されてほしくない要素がしばらく画面上に残るということがあります。
閉じる際のアニメーションを残したサンプルを Google Chrome のような overlay プロパティ対応のブラウザ3で開き、ソース表示のペインを非表示から表示に変更すると、次の動画のように一度バナー部分が下に伸びてから、縮んで正常な高さに戻るのが確認できると思います。
スマホサイズ画面の場合は、メディアクエリで高さを限定することで抑制しているのですが、大きな画面の場合、例えば最近の Chrome には分割ビューという機能があり、リンクを右クリックして「分割ビューでリンクを開く」を選択するとひとつのタブが二分割されて両ページを一度に見られますが、その際に元ページの幅が突然半分になってブレイクポイント以下に…というのは十分考えられるシチュエーションですから、できれば抑制しておきたいところです。
もう少し実装を工夫すれば(例えば高さ固定にするなど)少なくともバナーの高さが伸び縮みするのは避けることもできそうですが、今回は時間がなかったため、とりあえず閉じる際のアニメーションを無効にしたものの方を完成形としておきました。余裕があれば後日対応してみますが、まぁこれはワンソースでがんばってるので致し方ない部分でもありますね…
まとめ
- 狭い画面ではハンバーガー・広い画面では横並びになるナビゲーションは、現在では(フォールバックを除けば)HTML と CSS のみで実装できます
- メニュー部分のマークアップに
<dialog>要素を使うことで、アクセシビリティ対応など多くの考慮事項はほぼすべてやってくれます - ハンバーガーメニューの開閉も今は
<button>要素に属性を追加するだけで JavaScript を書くことなく実現できます (Invoker Commands API) - 画面幅に応じたメニューの切り替えは CSS の
display: contentsを使えば、広い画面と狭い画面用に別々のメニューを用意する必要はありません - ただし現時点ではこれらの Web 標準に非対応の古いブラウザが存在するので、当面は JavaScript によるフォールバックが必要です
- また閉じる時のアニメーションを付けると、ビューポートのサイズが変更になった際にブラウザによっては意図せぬ挙動をするため、実装の際は注意しましょう
そんな訳で
「ハンバーガーメニュー css html のみ」などで検索するといっぱい出てくるせいで AI までが紹介してくる input:checked と兄弟セレクタを使った古くて非アクセシブルな方法をオレはいいかげん撲滅したいので、よろしければ拡散やブックマークなどでこちらの方法を広めていただけますと幸いです。涙