ヒントが表示されるポップオーバー
クリックJavaScriptボタン
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
アイコンをクリックするとそのヒントが表示されるポップオーバーUIの作り方です。
実装のポイント
2026年2月現在、2つのアプローチがあります。HTMLとCSSのみの方法と、従来ながらのJavaScriptを使った方法です。前者はFirefoxの完全なアニメーションをサポートしないのであれば採用可能です(動作に支障はありません)。
いずれの方法でもポップオーバー要素を使っています。ポップオーバー要素は最上位に来るので他の要素との重なりを考慮する必要がありません。また、ポップオーバーが表示されるのは1つのみなので、他のポップオーバーが表示されれば前のポップオーバーは自動的に閉じます。ほかにもエスケープキーや外側をクリックで閉じるなどの仕組みがあらかじめ組み込まれているのでこのようなヒントを表示するUIに便利です。
コード例
HTMLとCSSのみの方法
まずはHTMLとCSSのみで実装する方法を紹介します。そう遠くない内にこの方式になるでしょうから。
HTML
<button
class="questionButton"
type="button"
command="toggle-popover"
commandfor="answer"
>
<img src="/assets/icon_question.svg" alt="Question" width="32" height="32" />
</button>
<div id="answer" class="answerPopover" popover="auto">
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
</p>
</div>
CSS
.questionButton {
anchor-name: --question-button;
display: grid;
place-items: center;
width: 32px;
height: 32px;
border-radius: 50%;
color: var(--color-text);
}
.answerPopover {
--ease-out-quart: cubic-bezier(0.22, 1, 0.36, 1);
--ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);
--duration: 0.3s;
position: fixed;
position-anchor: --question-button;
position-area: top right;
width: 232px;
height: auto;
padding: 12px;
border: 1px solid var(--color-border-thin);
border-radius: 16px;
transform-origin: bottom left;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
/* 出現・退場アニメーション */
transition:
opacity var(--duration) var(--ease-out-quart),
height var(--duration) var(--ease-out-quart),
scale var(--duration) var(--ease-out-back),
translate var(--duration) var(--ease-out-quart),
filter var(--duration) var(--ease-out-quart),
display var(--duration) allow-discrete;
opacity: 0;
scale: 0.5;
translate: -16px 16px;
filter: blur(4px);
&:popover-open {
opacity: 1;
transform: none;
scale: 1;
translate: 0 0;
filter: blur(0);
@starting-style {
opacity: 0;
scale: 0.5;
translate: -16px 16px;
filter: blur(4px);
}
}
}
ポップオーバー要素の開閉は<button>タグのcommand属性およびcommandfor属性で操作できます。ポップオーバー要素の出現・退場アニメーションはtransitionプロパティにdisplayプロパティに対してallow-discreteを指定しているのがポイントです。平たく言うとdisplayプロパティをアニメーション可能にします。これにより表示時はすぐに表示し、非表示時はアニメーションが終わるまで表示を維持します。
またあわせて@starting-styleで表示時の初期値のプロパティを設定し表示のアニメーションを設定します。displayのような瞬間的な表示はトランジションアニメーションが効かないので、@starting-style開始時の状態を設定しておくでアニメーションが可能になります。
細かい取り回しも含めたものをHTML・CSSのみでできるのは画期的ですね。
JavaScriptを用いた方法
つづいて従来的なJavaScriptを用いた方法を紹介します。デモもこちらの方法で実装しています。allow-discreteに対応していないブラウザなどある程度古いブラウザでもアニメーションが動きます。
HTML
<button class="questionButton" type="button" id="popoverTrigger">
<img src="/assets/icon_question.svg" alt="Question" width="32" height="32" />
</button>
<div id="answer" class="answerPopover" popover="manual">
<p class="innerText">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Quisquam, quos.
</p>
</div>
CSS
.questionButton {
anchor-name: --question-button;
display: grid;
place-items: center;
width: 32px;
height: 32px;
border-radius: 50%;
color: var(--color-text);
}
.answerPopover {
--ease-out-quart: cubic-bezier(0.22, 1, 0.36, 1);
--ease-out-back: cubic-bezier(0.34, 1.56, 0.64, 1);
--duration: 0.3s;
position-anchor: --question-button;
position: fixed;
position-area: top right;
width: 232px;
height: auto;
padding: 12px;
border: 1px solid var(--color-border-thin);
border-radius: 16px;
transform-origin: bottom left;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
/* 出現・退場アニメーション */
transition:
opacity var(--duration) var(--ease-out-quart),
height var(--duration) var(--ease-out-quart),
scale var(--duration) var(--ease-out-back),
translate var(--duration) var(--ease-out-quart),
filter var(--duration) var(--ease-out-quart);
opacity: 0;
scale: 0.5;
translate: -16px 16px;
filter: blur(4px);
&:popover-open {
opacity: 1;
transform: none;
scale: 1;
translate: 0 0;
filter: blur(0);
@starting-style {
opacity: 0;
scale: 0.5;
translate: -16px 16px;
filter: blur(4px);
}
/* JSで閉じる時に退場アニメーションを適用 */
&.is-closing {
opacity: 0;
scale: 0.5;
translate: -16px 16px;
filter: blur(4px);
}
}
}
JavaScript
const trigger = document.querySelector(".questionButton");
const popover = document.querySelector(".answerPopover");
const closePopover = () => {
if (!popover.matches(":popover-open")) {
return;
}
popover.classList.add("is-closing");
const duration = 400; // style.css の --duration に合わせる
setTimeout(() => {
popover.classList.remove("is-closing");
popover.hidePopover();
trigger?.setAttribute("aria-expanded", "false");
}, duration);
};
trigger?.addEventListener("click", () => {
if (popover.matches(":popover-open")) {
closePopover();
} else {
popover.showPopover();
trigger?.setAttribute("aria-expanded", "true");
}
});
// 外側クリックで閉じる
document.addEventListener("click", (e) => {
// ポップオーバーが開いていない場合は何もしない
if (!popover.matches(":popover-open")) {
return;
}
// ボタンやポップオーバーの中にクリックされた場合は何もしない
if (trigger?.contains(e.target) || popover.contains(e.target)) {
return;
}
closePopover();
});
// ESCキーで閉じる
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && popover.matches(":popover-open")) {
closePopover();
}
});
開閉を手動でJavaScriptで制御する関係上、popover属性の値を"manual"にしています。こうすることでポップオーバー要素を手動で制御します。その分、外側クリックやESCキーで閉じる処理を自前で書いています。このあたりを見てもHTML・CSSのみで書けると非常に楽です。