Basic Popover
A click-triggered panel anchored to its button, with Escape and outside-click dismiss.
6 个框架中级
A popover with a heading, body copy and an action - labelled by its own heading.
<!--
This is the point where a tooltip stops being viable. There is a button
inside, and a tooltip may not contain interactive content - you cannot hover
your way to a control that disappears when the pointer leaves the trigger.
So it is role="dialog", click-triggered, and labelled by its own heading via
aria-labelledby instead of a hand-written aria-label.
-->
<div class="rpop">
<button
class="rpop__trigger"
type="button"
aria-haspopup="dialog"
aria-expanded="false"
aria-controls="rpop-panel"
>
What is a seat?
</button>
<div
class="rpop__panel"
id="rpop-panel"
role="dialog"
aria-labelledby="rpop-title"
hidden
>
<h3 class="rpop__title" id="rpop-title">Seats and billing</h3>
<p class="rpop__copy">
A seat is one person with access to this workspace. Seats are billed
monthly and prorated the day someone joins.
</p>
<a class="rpop__cta" href="#billing">Read the billing guide</a>
</div>
</div>
<script>
document.querySelectorAll('.rpop').forEach(function (root) {
var trigger = root.querySelector('.rpop__trigger');
var panel = root.querySelector('.rpop__panel');
function setOpen(open) {
panel.hidden = !open;
trigger.setAttribute('aria-expanded', String(open));
}
trigger.addEventListener('click', function () {
var next = trigger.getAttribute('aria-expanded') !== 'true';
setOpen(next);
// Move into the panel on open: the reason this popover exists is the
// thing inside it, and Tab from the trigger should not have to walk past
// the rest of the page to get there.
if (next) root.querySelector('.rpop__cta').focus();
});
root.addEventListener('keydown', function (event) {
if (event.key !== 'Escape') return;
setOpen(false);
trigger.focus();
});
document.addEventListener('mousedown', function (event) {
if (!root.contains(event.target)) setOpen(false);
});
});
</script>| Prop | 类型 | 默认值 | 说明 |
|---|---|---|---|
label必填 | string | - | 加载时朗读的无障碍标签。 |
title必填 | string | - | 卡片的标题文本。 |
copy必填 | string | - | 显示在标题下方的正文文本。 |
ctaLabel | string | 'Learn more' | 行动号召按钮的文案。 |
onClick | () => void | - | 用户激活控件时触发。 |
The button inside is exactly why this is a `role="dialog"` and not a tooltip: a tooltip may not contain interactive content, because you cannot hover your way into a bubble that closes the moment the pointer leaves the trigger. That single fact decides the whole component - click-triggered, focus moved into the panel on open, `aria-labelledby` pointing at the visible `<h3>` rather than a hand-written `aria-label` that will drift from it. The focus move uses `requestAnimationFrame` because the panel does not exist until React commits the render. Note the link colour flips to `blue-300` on dark: `blue-700` is tuned for white and fails AA outright on a near-black panel.