CSSとpopover属性で、画面の端からメニューを出す方法
画面の端からすべり出るメニューは、HTML の popover 属性と、CSS の transform と transition で作れます。開け閉めはブラウザが受け持つので、JavaScript は書きません。メニューの外側をクリックしたときや Esc キーを押したときにも、メニューは閉じます。スマートフォン向けのページで、ハンバーガーメニューのボタンを押すとリンクの一覧が画面の横から出てくる、いわゆるドロワーを作りたい場面で使えます。
検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。HTML と CSS のページを Playwright で開き、主に幅390px・高さ700pxの画面で、getBoundingClientRect() でメニューの位置を測りました。Firefox と Safari、スマートフォンの実機、スクリーンリーダーでは試していません。

コード全体
HTML は、開くボタンと、メニューの nav を書きます。nav に popover 属性を付け、ボタンの popovertarget に nav の id を書きます。
<header>
<button type="button" popovertarget="menu">メニュー</button>
<span>サイト名</span>
</header>
<nav id="menu" class="drawer" popover>
<button type="button" popovertarget="menu" popovertargetaction="hide">閉じる</button>
<ul>
<li><a href="#home">ホーム</a></li>
<li><a href="#about">このサイトについて</a></li>
<li><a href="#contact">お問い合わせ</a></li>
</ul>
</nav>
<main>
<p>本文の段落です。左上のボタンを押すと、画面の左端からメニューが出てきます。</p>
<p><a href="#body-link">本文の中のリンク</a></p>
</main>
CSS は次のとおりです。色や影などの見た目の CSS は省いています。
.drawer {
position: fixed;
inset: 0 auto 0 0;
width: min(80vw, 300px);
height: 100dvh;
max-height: none;
margin: 0;
border: none;
padding: 16px;
box-sizing: border-box;
transform: translateX(-100%);
transition:
transform 0.3s ease,
overlay 0.3s allow-discrete,
display 0.3s allow-discrete;
}
.drawer:popover-open {
transform: translateX(0);
}
@starting-style {
.drawer:popover-open {
transform: translateX(-100%);
}
}
@media (prefers-reduced-motion: reduce) {
.drawer {
transition: none;
}
}
後ろのページを暗くしたいときは、.drawer::backdrop { background: rgba(0, 0, 0, 0.4); } のように、::backdrop に色を付けます。上の画像では、この指定でページを暗くしています。
「メニュー」ボタンを押して開き、メニューの外側を押して閉じる様子が、次の画像です。

GIF の上の帯とマウスポインターは、画像を作るときに後から重ねたものです。すべる0.3秒の動きは、transition を止めて25ミリ秒ずつ進めながら撮ったので、GIF では実際よりゆっくり動きます。
popover 属性で、開け閉めをブラウザに任せる
ポップオーバー API の使い方の説明にあるとおり、popover 属性を付けた要素は、ページを開いた時点では display: none で隠れています。popovertarget を書いたボタンを押すと、表示と非表示が切り替わります。popovertargetaction="hide" を書いた「閉じる」ボタンは、閉じるだけのボタンになります。
値を書かない popover は popover="auto" と同じで、メニューの外側をクリックしたときや Esc キーを押したときにも閉じます。検証環境で試したところ、メニューの外側を押したとき、Esc キーを押したとき、「閉じる」ボタンを押したときのどれでも、メニューは閉じて display: none に戻りました。
開いたメニューは、最上位レイヤーという、ページのほかの要素よりも上の層に出ます。ヘッダーに position: sticky と z-index: 9999 を付けたページで開いても、ヘッダーの上(x=100、y=20)に出ていたのはメニューでした。メニューのほうには z-index を書いていません。
開くボタンには aria-expanded 属性を書いていませんが、Chrome DevTools Protocol の Accessibility.getFullAXTree で開くボタンのアクセシビリティの情報を読むと、閉じているときは expanded が false、開いているときは true でした。
画面の左に固定し、transform で外へずらしておく
メニューは position: fixed と inset の 0 auto 0 0 で、画面の上・下・左に付けます。幅は min(80vw, 300px) にしました。画面の幅が390pxと800pxのときはメニューの幅が300px、320pxのときは256px(画面の80%)で、狭い画面でも右側にページが少し見えます。
inset から margin までを書かずに開くと、メニューは画面の真ん中に、中身の大きさで出ました。そのときの値を読むと、inset が 0px、margin が上下242.6px・左右103.2pxで、ブラウザが上と下、左と右の余白をそれぞれ同じにして、真ん中に置いていました。端に付けるには、margin: 0 と inset を書き直す必要があります。
閉じているときの位置は、transform の translateX(-100%) で決めます。% は、transform-box を指定していないときはメニュー自身の幅に対する割合で、幅300pxのメニューでは左端が-300pxになりました。幅が変わっても、ちょうど幅の分だけ左の画面の外へずれます。開いたとき(:popover-open)は translateX(0) にして、画面の中に戻します。
「メニュー」ボタンや外側を押してから、メニューの左端の位置を約50ミリ秒ごとに測りました。
| 押してから | 開くとき(左端) | 外側を押して閉じるとき(左端) |
|---|---|---|
| 0ミリ秒前後 | -300px | 0px |
| 約110ミリ秒 | -100.1px | -172.7px |
| 約210ミリ秒 | -15px | -277.4px |
| 約310ミリ秒 | 0px | -300px |
| 約360ミリ秒 | 0px | 隠れた(display: none) |
開くときは画面の外の-300pxから0pxへ、閉じるときは0pxから-300pxへ、0.3秒かけて動きました。
メニューが長いときは、メニューの中をスクロールできます。リンクを40個に増やして開くと、メニューの高さは画面と同じ700pxで、中身の高さは1939pxでした。本文も高さ3000pxにして、メニューの上でホイールを600pxずつ回すと、3回目でメニューの中がいちばん下(1239px)まで届きました。4回目は、メニューの中は動かず、代わりに後ろのページが600pxスクロールしました。
メニューの端まで来たら後ろのページを動かさないようにするには、メニューに overscroll-behavior を足します。
.drawer {
overscroll-behavior: contain;
}
これを足して同じように4回回すと、4回目も後ろのページは0pxのままでした。
開く動きは @starting-style、閉じる動きは display と overlay
MDN の @starting-style の説明にあるとおり、display: none から表示に変わった要素には、ふつうは transition がかかりません。@starting-style の中に「表示した直後の値」として translateX(-100%) を書くと、そこから translateX(0) へ動きます。@starting-style を消して試すと、押した直後からメニューは0pxにあり、すべる動きはありませんでした。
閉じるときは、:popover-open が外れた瞬間に display: none になると、すべる動きが見えません。transition に display と overlay を入れて allow-discrete を付けると、display: none への切り替えが、transition が終わるまで遅れます。overlay は、最上位レイヤーから外すのを同じように遅らせる指定です。上の表で、閉じるときに約310ミリ秒まで display が block のままだったのは、この指定のためです。
transition を transform 0.3s ease だけにして試すと、外側を押した直後から display は none になり、メニューはすべらずに消えました。
@media (prefers-reduced-motion: reduce) の中では、transition: none にしています。OS などで動きを減らす設定をしている人には、メニューがすぐに出ます。Playwright でこの設定にして開くと、押した直後からメニューは0pxにありました。
transform だけで隠すと、Tab キーで見えないリンクに入る
popover を使わず、クラスを付け外しして transform だけで隠すメニューも作れます。ただし、隠しているのは見た目だけです。閉じた状態で Tab キーを押していくと、「メニュー」ボタンの次に、画面の外(左端が-284px)にある「閉じる」ボタンや「ホーム」のリンクにフォーカスが移りました。キーボードで操作する人には、見えない場所にフォーカスが行ってしまいます。
popover を使ったメニューは、閉じているあいだ display: none なので、Tab キーで入りません。閉じた状態で Tab キーを押すと、「メニュー」ボタンの次は本文のリンクでした。開いた状態では、「メニュー」ボタンの次に「閉じる」ボタン、「ホーム」などのリンクの順にフォーカスが移り、そのあと本文のリンクに進みました。
右の端から出すときは、向きを逆にする
右の端から出すときは、上の CSS の後ろに次の CSS を足します。メニューを画面の右に付け、translateX(100%) で右の画面の外へずらします。
.drawer {
inset: 0 0 0 auto;
transform: translateX(100%);
}
@starting-style {
.drawer:popover-open {
transform: translateX(100%);
}
}
幅390pxの画面で開くと、メニューの左端は390pxから90pxへ動き、右の端に幅300pxで付きました。開いた後も、ページの横幅(scrollWidth)は390pxのままで、横のスクロールは出ませんでした。
対応しているブラウザ
MDN の互換性データ(@mdn/browser-compat-data 8.1.8)では、次のバージョンから使えます。
| 機能 | Chrome | Firefox | Safari |
|---|---|---|---|
popover 属性 |
114 | 125 | 17 |
@starting-style |
117 | 129 | 17.5 |
transition-behavior(allow-discrete) |
117 | 129(一部) | 17.4 |
overlay |
117 | なし | なし |
Firefox の transition-behavior には、「display を allow-discrete で transition させることには対応していない」という注記が付いています。overlay は、Firefox と Safari では「なし」になっています。





