About

Popover APIとは?JSなしでツールチップを開閉する方法

popover属性1つで開閉・Escキー・アクセシビリティが自動動作するブラウザネイティブUI

この記事を読むと、ツールチップ・メニュー・トーストをJavaScriptなしで開閉し、ボタンのすぐ下に置けるようになる。モーダルはpopoverではなくdialog、という線引きも。

<button popovertarget="tip-1">ヘルプ</button>
<div id="tip-1" popover="auto" role="tooltip">
  ネイティブツールチップ
</div>

JavaScript 0行。イベントリスナーなし。状態追跡なし。それでもEscで閉じ、キーボードでアクセスでき、スクリーンリーダーが正しく読み上げる。

先にひとつだけ。これはボタンを押したときに開く(キーボードのEnter・Spaceも含む)。マウスを乗せただけで出るツールチップは、まだJavaScriptの仕事だ。下の「まだJavaScriptが必要な領域」でまた出てくる。

従来のツールチップが壊れる理由

「ホバーで表示、離れたら非表示」という単純な要件。実際にデプロイすると問題が噴出する。

キーボードユーザーがTabでトリガーに到達してもツールチップが出ない。スクリーンリーダーが2回読むか、全く読まない。マウスを速く動かすとちらつく。小さい画面で重なる。Escで閉じない。フォーカスが失われる。

イベントリスナーが積み重なる。hover/focus別々の処理。外部クリックの特殊ケース。ARIA属性の手動同期。コードが肥大化する。実装の問題ではなく、Webプラットフォームに「この要素はツールチップだ」という概念がなかったからだ。

Popover APIが変えたこと

popover属性を付けるとブラウザがこの要素の役割を理解する。3つが自動で解決。

1. JavaScriptなしで開閉

popovertarget="id"でボタンとpopoverを接続するだけ。クリック(Enter・Spaceを含む)での開閉をブラウザが処理する。popovertargetactionでshow、hide、toggle(デフォルト)を指定可能。

2. Escキー自動処理

グローバルkeydownハンドラ不要。Esc専用のクリーンアップロジック不要。ブラウザがpopoverを解除可能な要素と認識する。

3. ARIA状態の自動同期

トリガーのボタンが「いま開いている/閉じている」をスクリーンリーダーに伝えるのはブラウザの担当になる。DOMにaria-expanded属性が付くわけではなく、アクセシビリティツリーの側で処理される。だからJSで属性を合わせる必要がなく、開閉状態とずれることもない。

z-index合戦が終わる — top layer

開いたpopoverはtop layerに描かれる。ページの他のすべての上にある別の層なので、z-index: 9999の要素よりも上に出る。親にoverflow: hiddenがかかっていても切れない。

カードの中のドロップダウンが切れて、結局DOMをbodyの末尾に移したことがある人には、ここがいちばん嬉しいところだと思う。

背景を少し暗くしたいなら::backdropを使う。

[popover]::backdrop {
  background: rgb(0 0 0 / 0.3);
}

フォーカスは閉じ込めない — モーダルならdialog

popoverが開いていても、Tabで外の要素に移れる。後ろのページを触れなくする(フォーカストラップ)こともしない。

確認ダイアログのように「これを終えるまで他を触らせない」なら、<dialog>をshowModal()で開く。こちらは残りのページをinert(操作不可)にしてくれる。

選び方は単純。ツールチップ・メニュー・トーストはpopover、答えないと先に進めない窓はmodal dialog。

auto vs manual

<!-- auto: 外側クリックで閉じる -->
<div popover="auto">auto</div>

<!-- manual: 明示的に閉じるまで残る -->
<div popover="manual">manual</div>

autoは外部クリック、Esc、別のauto popover表示で自動的に閉じる。ツールチップ向け。

manualは外側クリックでもEscでも閉じない。ボタン(popovertarget)かhidePopover()で閉じる。そのかわり複数を同時に出せるので、トースト通知に向いている。

ボタンが何個あっても同じpopoverを開ける

popovertargetはidを指しているだけなので、同じidを指すボタンをいくつ置いてもいい。ヘッダーの「?」と本文のボタンが同じヘルプを開く、という使い方ができる。popovertargetが効くのは<button>(と<input type="button">)だけで、<a>に付けても何も起きない。

開くだけのボタン、閉じるだけのボタンはpopovertargetactionで分ける。showは開くだけ、hideは閉じるだけ、デフォルトのtoggleは開いていれば閉じる。

<button popovertarget="help">?</button>
<button popovertarget="help" popovertargetaction="show">ヘルプを見る</button>

<div id="help" popover>
  ...
  <button popovertarget="help" popovertargetaction="hide">閉じる</button>
</div>

位置はCSSで — 開いたボタンがそのままアンカー

最初にpopoverを出してみると、画面のど真ん中に出る。デフォルトのスタイルがposition: fixed + margin: autoだからだ。ツールチップならボタンの横にいてほしい。

popovertargetでつなぐと、そのボタンがpopoverのアンカー(「この要素を基準に置く」の基準点)になる。だからanchor-nameを付けなくても、position-areaの1行でボタンのすぐ下に置ける。

[popover] {
  margin: 0;
  position-area: bottom;
}

Default position: fixed; margin: auto Help popover far from the button Anchored margin: 0; position-area: bottom Help popover the button is the implicit anchor

margin: 0は忘れやすい。デフォルトのmargin: autoが残っていると位置がずれる。anchor()やanchor-centerで細かく置くときは、position-anchor: autoも書かないとこの暗黙のアンカーは使われない(MDN)。

CSS anchor positioningを知らないブラウザではこの行が無視され、また画面の真ん中に出る。それで困るUIなら、下の「ライブラリ」の出番だ。

まだJavaScriptが必要な領域

タイミング制御。ネイティブpopoverは即座に開閉する。マウスを速く動かすとちらつく問題は残る。ディレイが必要ならJavaScript。

ホバーインテント。ブラウザはポインターがなぜ要素上にあるか分からない — 意図的か通過中か判断できない。

ただし核心が変わった。JavaScriptはもう相互作用モデル全体を所有しない。Popover APIが基本開閉を処理し、JavaScriptはその上にインテント判別だけを載せる。

ライブラリが依然有効な場合

複雑なポジショニング — ネストしたスクロールコンテナ間の衝突検出、カスタムフリッピングロジックが必要ならFloating UIが依然優位。「ボタンの下」程度なら上のposition-areaで足りるが、anchor positioningを知らないブラウザでも同じ位置に出したいなら、まだライブラリのほうが楽。

大規模デザインシステム — チーム横断で一貫した動作とガードレールが必要な場合。

アクセシビリティに慣れていないチーム — Popover APIのデフォルトは良いが、role・ラベル・フォーカス管理をいつ足すかは自分で判断する必要がある。

ほとんどの場合 — 単純なツールチップ、ドロップダウンメニュー、トースト通知 — Popover APIだけで十分だ。ライブラリなしで。

既存画面の自作ツールチップやモーダルをpopover・dialogに置き換えていて詰まったら、negabaroに気軽に聞いてほしい。

キーポイント

1

popover属性とpopovertargetだけで開閉・Esc・キーボードナビゲーションが自動動作

2

開閉状態はブラウザがアクセシビリティツリーに伝える — ARIA属性をJSで合わせる必要がない

3

開いたpopoverはtop layerに出る — z-indexやoverflow: hiddenで切れない。モーダル(フォーカストラップ)が要るならdialog.showModal()

4

autoは外側クリック/Escで閉じて1つだけ表示、manualはボタンかhidePopover()でだけ閉じて複数同時に出せる

5

popovertargetのボタンがそのままアンカー — position-area: bottomの1行でボタンの下に置ける(margin: 0を忘れずに)

6

タイミング制御やホバーインテントは依然JavaScriptが必要だが、核心モデルはブラウザ担当

ユースケース

単純ツールチップ — popover="auto" + role="tooltip"でJS不要 ドロップダウンメニュー — popovertargetactionで開閉制御 トースト通知 — popover="manual"で明示的制御
← Intersection Observer — スクロールイベントなしで要素の可視性を検出 background-removal-jsとは?ブラウザだけで画像の背景を削除して透明PNGにする仕組み →