3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

popover で作るピュアCSS階層ハンバーガーメニュー

Last updated at Posted at 2025-12-18

概要

 最近実装されたpopoverや、CSSの新しい機能を使って、ピュアCSSで階層ハンバーガーメニューを作成する方法を解説します。

サンプル

Chrome で見ていただけるとうれしいです。

See the Pen Untitled by lhankor_mhy (@lhankor_mhy) on CodePen.

サンプルコード
<div class="conponent-root">
    <style>
        @scope to (.conponent-root) {
            :scope {
                position: sticky;
                inset: 0 auto auto;
            }

            button {
                all: unset;
                display: block;
                text-align: center;
                font-size: 2rem;
                padding: 1rem;
            }

            [popover] {
                all: unset;
                position: fixed;
                inset: 0;
                width: fit-content;
                height: 100vh;

                transition: all .7s .2s allow-discrete;
                translate: -100%;

                &:popover-open {
                    translate: 0;
                    color: white;
                    background-color: black;
                }
            }

            ::backdrop {
                transition: all .7s .2s allow-discrete;
            }

            :popover-open::backdrop {
                background-color: oklch(0 0 0 / .3);
                backdrop-filter: blur(.5em);

                @starting-style {
                    background-color: transparent;
                    backdrop-filter: blur(0em);
                }
            }

            menu {
                width: 100%;
                display: grid;
                grid: auto-flow / minmax(min-content, 25vw);
                place-items: start;
                place-content: start;
                padding: 1em;

                li {
                    display: block;
                    padding: .4em;
                }
            }
        }
    </style>
    <button popovertarget="hamburger" popovertargetaction="show"></button>
    <div popover id="hamburger">
        <button popovertarget="hamburger" popovertargetaction="hide">×</button>
        <menu>
            <li>HOME</li>
            <li>

                <div class="conponent-root">
                    <style>
                        @scope to (.conponent-root) {
                            button {
                                all: unset;
                                anchor-name: --submenu-button;
                            }

                            [popover] {
                                position: fixed;
                                position-area: center right;
                                inset: 0 0 0 1em;
                                width: fit-content;
                                height: 0;
                                overflow: hidden;

                                background-color: transparent;
                                border-color: white;

                                transition: all .7s .2s allow-discrete;
                                interpolate-size: allow-keywords;

                                &:popover-open {
                                    height: fit-content;
                                    color: white;
                                    background-color: black;

                                    @starting-style {
                                        height: 0;
                                        color: inherit;
                                        background-color: transparent;
                                    }
                                }
                            }

                            menu {
                                width: 100%;
                                display: grid;
                                grid: auto-flow / minmax(min-content, 25vw);
                                place-items: start;
                                place-content: start;
                                padding: 1em;

                                li {
                                    display: block;
                                    padding: .4em;
                                }
                            }
                        }
                    </style>
                    <button popovertarget="submenu" popovertargetaction="toggle">Contents ▶</button>
                    <div popover id="submenu">
                        <menu>
                            <li>products</li>
                            <li>service</li>
                            <li>blog</li>
                        </menu>
                    </div>
                </div>
            </li>
            <li>News</li>
            <li>About us</li>
            <li>Contact</li>
        </menu>
    </div>
</div>

解説

ポップオーバーとは

 ポップオーバーは、イメージとしては軽めのモーダルダイアログのようなものです。用途としては、トーストやツールチップ、メニューなどに向いています。
 特徴としては以下の通りです。

  • 必ず一番上に表示される(トップレベルレイヤー)
  • ボタンやESCキーなどで閉じることができるほか、背景クリックでも閉じることができる(light dismiss)
  • CSSの疑似クラスを使って、開閉状態をスタイリングできる
  • ポップオーバーを開くと、他のポップオーバーは自動で閉じる(入れ子ポップオーバーを除く)

HTMLでポップオーバーを作る

    <button popovertarget="hamburger" popovertargetaction="show"></button>
    <div popover id="hamburger">
        <button popovertarget="hamburger" popovertargetaction="hide">×</button>
...        
    </div>

 popover属性を持つ要素がポップオーバーになります。
 ポップオーバーの開閉はボタンで制御できます。つまり、popover属性を持つ要素とbutton要素の二つを用意すれば実装できてしまうのです。お手軽ですね。

 ボタンには最低限popovertarget属性を用意します。この属性には開閉対象のポップオーバーのIDを指定して紐づけます。
 オプションとして、popovertargetaction属性にはshow(開く)、hide(閉じる)、toggle(開閉切り替え)のいずれかを指定します。指定しない場合はtoggle扱いです。

CSSでポップオーバーをスタイリングする

[popover] {
    all: unset;
    position: fixed;
    inset: 0;
    width: fit-content;
    height: 100vh;

    transition: all .7s .2s allow-discrete;
    translate: -100%;

 特に言うことのないスタイルが並んでいると思います。属性セレクタ[popover]で要素を指定し、all: unsetでデフォルトスタイルを消し、translate: -100%で画面外に隠しています。

allow-discrete

 allow-discreteは見慣れぬキーワードかもしれません。これが新しめの機能です。

 displayblocknoneに切り替えることを含むトランジションは、今までですと途中で消えてしまって上手く動作しませんでしたが、allow-discreteを指定することによってトランジションの間は表示し続けてくれます。

CSSでポップオーバーを開いた状態をスタイリングする

[popover] {
...
    &:popover-open {
        translate: 0;
        color: white;
        background-color: black;
    }

 ポップオーバーが開いたら、translateを戻して表示させています。その他、色をトランジションさせたりしていますがこれはまあおまけです。

:popover-open

 :popover-open疑似クラスが新機能です。

 開いた状態のポップオーバーにヒットする疑似クラスです。もー、めっちゃわかりやすい。[popover]:popover-openさえ書けば、ひとまずそれっぽいポップオーバーができます。
 この「開いた状態のポップオーバー」というのは、「見えている状態」とはまた別です。なので、CSSその他で無理やり表示させたりしてもこの疑似クラスにはヒットしません。

CSSでバックドロップをスタイリングする

 バックドロップは、ポップオーバーが開いたときにできる背景です。ダイアログを開いたときに背景が暗くなるUIとかあると思いますが、アレです。

::backdrop {
    transition: all .7s .2s allow-discrete;
}

:popover-open::backdrop {
    background-color: oklch(0 0 0 / .3);
    backdrop-filter: blur(.5em);

    @starting-style {
        background-color: transparent;
        backdrop-filter: blur(0em);
    }
}

::backdrop

 ::backdrop疑似要素は、バックドロップを指します。普段は表示されないのでここではトランジションの指定をしているだけです。allow-discreteをお忘れなく。

@starting-style

 :popover-open::backdropで、バックドロップが表示された時のスタイリングをします。「開いた状態のポップアップ」の「バックドロップ」ということですね。間に とか挟んじゃだめです。
 @starting-styleも見慣れないかもしれません。新しめの機能です。

 これは、display: noneなどに|display: noneなどから変化するときにどのスタイルへ変化するか、を指定します。display: noneは表示がされないのでスタイルが適用されないため、このような補助が必要ということですね。個人的には、気にせずに記述されているプロパティに変化してくれればいいのに、などと思うのですが、後方互換などいろいろな事情があるのでしょうきっと。

 あと、このようにセレクタの入れ子に書く方法と、入れ子にせずに外に書く方法がありますが、入れ子にした方がCSS詳細度的に安全かもしれません。

HTMLでサブメニューを作る

 階層メニューにするので、サブメニューを作っていきます
 注意するのは、ポップオーバーを入れ子にするようにHTMLを書くことです。そうしないと親メニューが閉じてしまいます。

<button popovertarget="submenu" popovertargetaction="toggle">Contents ▶</button>
<div popover id="submenu">
...
</div>

 HTMLは説明不要ですよね?

CSSでサブメニューのスタイルをする

 いろいろな表現が考えられると思いますが、今回は親メニューの右側にメニューを浮かせて、高さをトランジションしてみます。

[popover] {
    position: fixed;
    position-area: center right;
    inset: 0 0 0 1em;
    width: fit-content;
    height: 0;
    overflow: hidden;

    transition: all .7s .2s allow-discrete;

 allow-discreteをお忘れなく!
 あと、今回はデフォルトスタイルのリセットしてないです。大抵のブラウザであれば開いていないポップオーバーは非表示にしてくれていると思うので、それを利用しています。もしそうではないブラウザがあるのならきちんと記述しなくてはダメです。

position-area

 position-area、これも見慣れないかもです。
 この記述をすることによって、サブメニューを開いたボタンを基準にどこに表示するのかを指定できます。ここではposition-area: center rightなので、ボタンの垂直中央ぞろえ・水平右隣の指定になります。サブメニューっぽいでしょ?
 いろいろな事情により、position: fixedをセットで指定しておくことをおすすめします。

 また、詳しくは省略しますが、本来はanchor-nameposition-anchorで紐づけをしなくてはならないです。ポップオーバーとそのボタンは暗黙の紐づけができるのでここでは省略できます。

CSSで開いたサブメニューのスタイルをする

[popover] {
...
    interpolate-size: allow-keywords;

    &:popover-open {
        height: fit-content;
        color: white;
        background-color: black;

        @starting-style {
            height: 0;
            color: inherit;
            background-color: transparent;
        }
    }

interpolate-size: allow-keywords

 interpolate-size: allow-keywordsも見慣れないと思います。おまじないだと思って入れましょう(←やめろ)

 CSSでアコーディオンメニューとか作ったことがある人はわかると思いますが、height: 0height: autoってアニメーションしないんですよね。なので、max-heightgrid-template-rowsを動かしてアコーディオンメニューにしたりしてました。
 interpolate-size: allow-keywordsを書いておくとそれができてしまう。
 まさにおまじないです。
 なんなら継承もするのでルートにぶち込んでおいて忘れるのもよいかもです。

以上です!

 え、@scopeは説明しないのかって? 気にしない気にしない!

ブラウザ対応

 以上、解説しましたが、2025/12現在、スタイリングについては全てのブラウザでちゃんと動作するとは限らないです。“Baseline 2024”とはいったい……?

allow-discrete;

 Firefox で動作しないです! MDN などではフルサポートとなっていますが、騙されてはいけません。displayなどに対応してないとか、全く意味がないです。

@starting-style

 だいたい問題ないです!

::backdrop

 Firefox でアニメーションしないバグがあります!

interpolate-size: allow-keywords;

 Firefox と Safai で動作しないです!

position-area

 Firefox で動作しないです!

@scope

 だいたい問題ないと思いますが、Firefox は先週実装されたばかりです!

つまり

 Firefox さえ割り切れるならだいたい大丈夫です!

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?