HTMLとCSSで、マウスを乗せたときにポップオーバーを開く方法

​​CSS​HTML,Playwright,Popover API

ボタンやリンクに interestfor 属性を付け、ポップオーバーの id を指定すると、マウスを乗せたときにポップオーバーが開き、離すと閉じます。キーボードでフォーカスしたときも開き、Esc キーで閉じます。開くまでの待ち時間は CSS の interest-delay で変えられ、JavaScript は要りません。ボタンの近くに短い説明を出すツールチップを作りたいときに使えます。

補足

検証環境は Ubuntu 24.04、Node.js 22.23.4、Playwright 1.63.0、Chromium 153 です。ヘッドレスの Chromium でページを開き、Playwright でマウスとキーボードを操作して、interest・loseinterest・toggle イベントが起きた時刻を記録しました。Firefox と Safari、スマートフォンの実機(長押しで開く動き)、スクリーンリーダーでの読み上げ、<area> と SVG の <a> は試していません。各ブラウザーの対応バージョンは、互換性データ(@mdn/browser-compat-data 8.1.8)の値です。

ボタンに interestfor、出したい要素に popover="hint" を付ける

もとになる書き方は次のとおりです。ボタンの interestfor に、出したい要素の id を書きます。出したい要素には popover="hint" を付けます。

html
<p>
  <button interestfor="tip">ヘルプ</button>
  下書きの保存について
</p>
<div id="tip" popover="hint">保存せずに閉じると、入力した内容は消えます</div>

popover 属性を付けた要素は、開くまで表示されません。MDN の関心インボーカーの解説によると、interestfor を付けた要素に、マウスを乗せる・フォーカスする・長押しするなどして「関心を示す」と、ブラウザーがポップオーバーを開きます。

検証環境で、マウスをボタンに乗せてから開くまでを3回測ると、502〜505ミリ秒でした。マウスを外へ出してから閉じるまでは、252〜254ミリ秒でした。ボタンに乗せて300ミリ秒で離したときは、ポップオーバーは開かず、イベントも起きませんでした。マウスがボタンの上を通り過ぎただけでは開きません。

ボタンとポップオーバーの画面のアニメーション。上に「操作 1回目/3回:ボタンに乗せる」のような表示があり、操作が変わった瞬間にピンクになる。矢印のカーソルが「ヘルプ」ボタンに乗ると、約0.5秒後に「保存せずに閉じると、入力した内容は消えます」というポップオーバーがボタンの下に開き、状態の表示が「開いた」とピンクになる。2回目でカーソルをポップオーバーの上へ移しても開いたまま。3回目でポップオーバーの外へ出すと、約0.25秒後に閉じて「閉じた」とピンクになる
マウスを乗せる、ポップオーバーへ移る、外へ出す、の3つの操作で、ポップオーバーが開いて閉じるまで

キーボードでも同じように動きました。Tab キーでボタンにフォーカスすると503ミリ秒で開き、次のボタンへ Tab で移ると252ミリ秒で閉じました。開いているときに Esc キーを押すと、フォーカスはボタンに残ったまま、ポップオーバーだけが閉じました。マウスで開いたときも Esc で閉じ、そのあとマウスを乗せたまま1秒待っても開き直しませんでした。

position-area でボタンのすぐ下に出す

上の HTML だけでは、ポップオーバーはボタンから離れた画面の中央に出ます。検証環境の 800×600 の画面では、ポップオーバーの左上の角が 373, 283 の位置に出ました。

ボタンの下に出すには、ポップオーバーに position-area を指定します。

css
#tip {
  position-area: bottom;
  margin: 4px;
}

MDN のポップオーバー API の解説によると、ポップオーバーとそれを開く要素を結びつけると、開く要素が暗黙のアンカーになります。そのため、anchor-name を書かなくても position-area だけで位置が決まります。interestfor で結びつけた場合も同じで、検証環境では、下端が 64px のボタンに対して、ポップオーバーは上端 68px の位置に出ました。

ポップオーバーの上へマウスを移しても閉じない

ボタンからポップオーバーの上へマウスを移すと、ポップオーバーは開いたままでした。ポップオーバーの上に1.5秒置いても閉じず、loseinterest イベントも起きませんでした。ポップオーバーの外へ出すと、252ミリ秒で閉じました。

ボタンとポップオーバーの間にすき間があるときは、すき間を通る時間しだいです。40px のすき間を150ミリ秒で通ったときは、開いたままでした。すき間で400ミリ秒止まってから移ったときは、着く前に閉じました。ボタンとポップオーバーは、あまり離さずに置きます。

待ち時間は interest-delay で変える

開くまでと閉じるまでの待ち時間は、ボタンなど interestfor を付けた側に interest-delay を指定して変えます。値を1つ書くと開くときと閉じるときの両方に、2つ書くと1つ目が開くまで、2つ目が閉じるまでの時間になります。片方だけ変えるときは interest-delay-start と interest-delay-end を使います。

css
button[interestfor] {
  interest-delay: 1s 2s;
}

検証環境で測った結果は次のとおりです。

指定 開くまで 閉じるまで
指定なし(normal) 502〜505ms 252〜254ms
interest-delay: 0s 17ms 2ms
interest-delay: 0.1s 103ms 102ms
interest-delay-start: 200ms 218ms 251ms
interest-delay: 1s 2s 1002ms 2002ms

初期値は normal で、getComputedStyle() で読んでも normal が返りました。MDN では、normal の待ち時間はブラウザーごとに違うことがあるとされています。上の約0.5秒と約0.25秒は、Chromium 153 での値です。負の値は書けず、CSS.supports('interest-delay-start', '-1s') は false でした。

ボタンが並んでいるところで待ち時間を長くすると、隣のボタンへ移るたびに待たされます。MDN の例にある、:has() と :interest-source を組み合わせた指定を足すと、どれか1つが開いている間だけ、ほかのボタンの待ち時間をなくせます。次の 1s 0.25s は、検証で使った値です。

css
button {
  interest-delay: 1s 0.25s;
}

p:has(button:interest-source) button {
  interest-delay-start: 0s;
}

検証環境で、<p> の中に並べたボタン1とボタン2に、それぞれ別のポップオーバーを結びつけました。ボタン1に乗せて開いてからボタン2へ移ると、:has() の指定がないときは、ボタン2のポップオーバーが開くまで1004ミリ秒かかりました。:has() の指定があるときは3ミリ秒で開き、ボタン1のポップオーバーも同時に閉じました。

開いている間だけ見た目を変える

:interest-source は interestfor を付けた要素に、:interest-target は interestfor で指定されたポップオーバーに、関心が示されている間だけ当てはまります。

css
button:interest-source {
  background: #d85878;
}

#tip:interest-target {
  border-style: dashed;
}

この2つは、マウスを乗せた瞬間ではなく、待ち時間が過ぎてポップオーバーが開いたときに当てはまりました。乗せて200ミリ秒の時点ではどちらも当てはまらず、ボタンの背景も初期の色のままでした。離してから100ミリ秒の、まだ閉じる前の時点では当てはまったままで、閉じると外れました。

popover="hint" にすると、開いているメニューを閉じない

popover 属性の値は、ほかのポップオーバーと一緒に使うときに効いてきます。値を書かない popover は auto と同じ扱いです。MDN のポップオーバー API の解説では、hint のポップオーバーを開いても auto のポップオーバーは閉じない、と説明されています。

検証環境で、クリックで開くメニュー(popover)を開いたまま、隣の「ヘルプ」ボタンにマウスを乗せました。ヒントが popover="hint" のときはメニューとヒントの両方が表示され、popover(auto)のときはヒントが開くと同時にメニューが閉じました。

2つの画面を縦に並べた画像。上の「ヒントが popover="hint"」では、メニューの中身と「ヘルプの説明」の両方が表示されている。下の「ヒントが popover(auto)」では、「ヘルプの説明」だけが表示され、メニューは閉じている。どちらも矢印のカーソルが「ヘルプ」ボタンに乗っている
メニューを開いたまま「ヘルプ」に乗せた結果を、hint と auto で比べた

同じボタンに、クリックで開く commandfor と interestfor の両方を付けることもできます。

html
<button interestfor="tip" commandfor="box" command="toggle-popover">設定</button>
<p id="tip" popover="hint">ヒント</p>
<div id="box" popover>設定の箱</div>

検証環境では、マウスを乗せてヒントが開いたあとにボタンをクリックすると、loseinterest が起きてヒントが閉じ、箱が開きました。そこからマウスをいったん離して乗せ直すと、ヒントが hint のときは箱を開いたままヒントが開きました。auto のときは、ヒントが開くと箱が閉じました。interestfor だけを付けたボタンやリンクでは、クリックしてもヒントは開いたままで、リンクはふつうにリンク先へ移りました。

JavaScript で中身を変える

ポップオーバーが開くときと閉じるときには、ポップオーバーの側で interest と loseinterest イベントが起きます(MDN の interest イベントの説明(英語))。イベントの source に、マウスを乗せたボタンやリンクが入ります。

js
const tip = document.getElementById("tip");

for (const link of document.querySelectorAll("a")) {
  link.interestForElement = tip;
}

tip.addEventListener("interest", (e) => {
  tip.textContent = `${e.source.textContent} の説明`;
});

interestForElement は、JavaScript から interestfor の相手を決めるプロパティです。検証環境では、2つのリンクに同じポップオーバーを設定すると、どちらに乗せても開きました。中身は「リンク1 の説明」「リンク2 の説明」と、乗せたリンクに合わせて変わりました。設定したあとの interestfor 属性は、空の文字列になりました。

interest イベントで preventDefault() を呼ぶと、ポップオーバーは開きませんでした。イベントの cancelable は true でした。

使える要素と、対応しているブラウザー

MDN では、interestfor を付けられるのは <a>、<button>、<area>、SVG の <a> とされています。検証環境では、<button> と href のある <a> では開きました。tabindex を付けた <span> と、href のない <a> では開きませんでした。

ヒントが開いている間に、Chromium のアクセシビリティツリーを Chrome DevTools Protocol(Accessibility.getFullAXTree)で取り出すと、「ヘルプ」ボタンの説明(description)にヒントの文が入っていました。

互換性データでは、各ブラウザーが次のバージョンから対応しています。

ブラウザー interestfor、interest-delay など popover="hint"
Chrome、Edge 142 151(133〜150 は古い仕様)
Android の Chrome 142 151(133〜150 は古い仕様)
Firefox 未対応 153(149〜152 は古い仕様)
Safari 未対応 Technology Preview
iOS の Safari 未対応 未対応

MDN の例では、対応していないブラウザーでも、interestfor を付けたリンクはふつうのリンクとして動くとされています。ヒントでしか見られない情報は置かず、ページの本文やリンク先でも同じことが分かるようにしておきます。

対応しているかどうかは、MDN の例と同じく interestForElement があるかで調べられます。検証環境では true が返りました。

js
const supported = Object.hasOwn(
  HTMLButtonElement.prototype,
  "interestForElement",
);

​Posted by 86