「HTML Popover API 実践ガイド」JSなしでツールチップ・メニュー・モーダルを作る
ツールチップ、ドロップダウン、確認ダイアログ。これまでJSで頑張ってきたUIが、HTMLの属性ふたつで動く時代に。Popover APIの実用パターンを一気に押さえます。

「クリックで開いて、外側クリックで閉じる」を毎回書いていた頭の悪さ
ドロップダウンメニュー、通知バナー、共有メニュー、確認モーダル。Web制作の現場で何度書いたか分からないUIですが、毎回ちょっとずつ書き方が違って、毎回ちょっとずつバグる。
外側クリックで閉じる処理、ESCキーで閉じる処理、開いたときのフォーカス管理、aria属性のトグル。地味に手間で、地味に間違えやすい。2年前のアパレル系ECサイトの案件で、ヘッダーメニューの開閉処理だけでQAから不具合チケットが7枚立ったときは、正直、心が折れました。
これを「HTML標準の機能」として一気に解決するのが Popover API。Chrome 114(2023年5月)、Safari 17(2023年9月)、Firefox 125(2024年4月)で対応済み。popover属性とpopovertarget属性、たったこの2つを書くだけで、「クリックで開いて、外側クリック・ESCで閉じる」が動きます。
最小構成は、HTML属性ふたつだけ
百聞は一見にしかず。まずはコードを見てください。
<!-- ボタン側:開きたい要素のidを popovertarget に書く -->
<button popovertarget="my-popover">開く</button>
<!-- ポップオーバー側:popover属性を付けるだけ -->
<div id="my-popover" popover>
ここに表示する中身
</div>
JSは一行もありません。ボタンにpopovertarget="my-popover"を書いて、対応する要素にpopover属性を付ける。これでクリックすれば開き、外側をクリックすれば閉じます。ESCキーでも閉じます。フォーカスも自動で管理されます。
初めて動かしたとき、本当にこれだけで動くのか半信半疑でDevToolsを開いて確認したくらいです。これは効きます。
ドロップダウンメニューを作る
Popoverの実務での出番として一番多いのが、メニュー系UIです。ヘッダーのユーザーメニュー、共有ボタンの選択肢、テーブル行のアクションメニュー。今までこれを@headlessui/reactのMenuコンポーネントに頼って実装していた人、けっこう多いはずです。
<button popovertarget="user-menu">メニュー ▾</button>
<div id="user-menu" popover class="menu">
<a href="/profile">プロフィール</a>
<a href="/settings">設定</a>
<hr>
<a href="/logout">ログアウト</a>
</div>
位置調整は今までだとJSで親要素の座標を計算してgetBoundingClientRect()でゴニョゴニョ、というのが定番でした。これもCSS Anchor Positioningと組み合わせれば、純粋にCSSだけで解決できます。
/* ボタン側:アンカー名をつける */
.menu-btn {
anchor-name: --menu-anchor;
}
/* ポップオーバー側:アンカーを基準に配置 */
.menu {
position-anchor: --menu-anchor;
top: anchor(bottom);
left: anchor(left);
margin-top: 8px;
}
確認モーダルを作る:<dialog>との使い分け
「本当に削除しますか?」のような確認モーダル。これ、HTMLには<dialog>要素もあって役割が重なって見えるんですが、現場での使い分けは明確です。
popover:補助的に開閉する軽量UI。ツールチップ、メニュー、通知、ヘルプテキストなど「ちょっと見せたい」UI。
モーダル的に背景を暗くしたい場合は<dialog>をshowModal()で開くのが筋。ただし、最近はPopoverでもbackdrop疑似要素が使えるので、軽い確認ダイアログ程度なら使えます。
<button popovertarget="confirm">削除する</button>
<div id="confirm" popover class="modal">
<h3>本当に削除しますか?</h3>
<p>この操作は元に戻せません。</p>
<button popovertarget="confirm" popovertargetaction="hide">
キャンセル
</button>
<button popovertarget="confirm" popovertargetaction="hide">
削除する
</button>
</div>
注目してほしいのがpopovertargetaction="hide"。これを付けたボタンは「閉じる専用ボタン」になります。show、hide、toggleの3種類が指定可能。JSでpopover.hidePopover()を書く必要が減ります。
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* popover専用の擬似要素で背景を暗くできる */
.modal::backdrop {
background: rgba(0, 0, 0, 0.4);
}
本当に削除しますか?
この操作は元に戻せません。アカウントに紐付いた全データが完全に消去されます。
開閉アニメーションを付ける:@starting-styleが鍵
標準のままだとパッと表示されるので、ちょっと味気ない。フェードインや拡大アニメを付けたいところです。ところが、popover要素はdisplay: noneとdisplay: blockを行き来する性質上、普通のtransitionが効きません。
.popover {
opacity: 0;
transition: opacity 0.2s;
}
.popover:popover-open {
opacity: 1;
}
/* displayが none ⇔ block で切り替わるため、 */
/* 開いた瞬間にはすでに opacity:1 になっていてアニメしない */
解決策は3点セット。transition-behavior: allow-discreteでdisplayプロパティもtransition対象にし、@starting-styleで「表示開始時の初期値」を別途定義します。
.popover {
opacity: 0;
transform: scale(0.95);
transition: opacity 0.2s, transform 0.2s,
display 0.2s allow-discrete,
overlay 0.2s allow-discrete;
}
.popover:popover-open {
opacity: 1;
transform: scale(1);
}
/* 表示開始時の初期値を定義 */
@starting-style {
.popover:popover-open {
opacity: 0;
transform: scale(0.95);
}
}
ハマりやすいのがoverlayプロパティの指定。popoverはtop-layer(最前面の特別な描画レイヤー)に乗るので、消える瞬間にtop-layerから外れるタイミングもtransitionに含めないと、アニメ途中で消えてしまう。これ、最初知らなくて30分ハマりました。
JSから操作する、フォールバックを書く
HTML属性だけで完結するのが魅力ですが、現場ではJSから開閉したい場面もあります。検索フィールドで結果が出たら自動で開く、API応答後に通知を表示する、など。
const popover = document.getElementById('my-popover');
// 開く */
popover.showPopover();
// 閉じる */
popover.hidePopover();
// トグル */
popover.togglePopover();
// 開閉時のイベント */
popover.addEventListener('toggle', (e) => {
if (e.newState === 'open') {
// 開いた直後の処理 */
}
});
非対応ブラウザの考慮も忘れずに。2024年4月でFirefoxが対応したのでモダンブラウザはほぼカバーできていますが、古いSafari(17未満)が残っている環境では、属性が無視されて常時表示になります。
if (!HTMLElement.prototype.hasOwnProperty('popover')) {
// 非対応:oddbird/popover-polyfill などを読み込むか、 */
// 古い実装にフォールバックする */
document.querySelectorAll('[popover]').forEach(el => {
el.hidden = true;
});
}
まとめ
- Popover APIは、HTMLのpopover属性とpopovertarget属性だけで、開閉できるUIを作れる新機能。Chrome 114、Safari 17、Firefox 125で全モダンブラウザ対応済み。
- 外側クリックで閉じる「light-dismiss」、ESCキーで閉じる、フォーカス管理、最前面への配置、これら全部をブラウザが自動でやってくれる。
- popover="auto"はメニューやツールチップ向け、popover="manual"はトーストや通知バナーなど明示的に閉じたいUIに向いている。
- popovertargetaction="hide"を付けたボタンは「閉じる専用ボタン」として動作する。JSで閉じる処理を書く必要が減る。
- 位置調整はCSS Anchor Positioningと組み合わせると、ボタンの真下や横にCSSだけで配置できる。
- 開閉アニメーションはtransition-behavior: allow-discreteと@starting-style、そしてoverlayプロパティの3点セットで実現する。
- ユーザーの返答が必須なモーダルは<dialog>、補助的な開閉UIはpopover、という使い分けが現場では筋がいい。
- 古いSafariが残る案件ではoddbird/popover-polyfillなどのpolyfillか、機能検出でのフォールバックを忘れずに。