bwc-popover
A non-modal floating panel anchored to a trigger button, built on the native
popover API. Use it for menus, tooltips with actions, or filters — anything
that floats without trapping focus or locking scroll.
import "basic-web-components/popover";Usage
Uncontrolled:
<bwc-popover>
<button slot="trigger">Options</button>
<div slot="popup">
<button data-close>Close</button>
</div>
</bwc-popover>Controlled, opening on top with an offset:
<bwc-popover id="p" side="top" side-offset="8">
<button slot="trigger">Options</button>
<div slot="popup">
<button data-close>Close</button>
</div>
</bwc-popover>
<script>
const p = document.getElementById("p");
p.onOpenChange = (open) => {
open ? p.setAttribute("open", "") : p.removeAttribute("open");
};
</script>Trigger clicks toggle; data-close clicks and native light-dismiss close.
Set open-on-hover to also open on trigger hover (with delay /
close-delay); the pointer may rest on the popup without closing it. Set
modal for a backdrop with scroll-lock and focus management.
While open, the trigger's native title is suppressed so only the custom
popover shows; it is restored on close.
The popup carries data-starting-style during the opening frames and
data-ending-style while closing, for author-driven enter/exit transitions:
bwc-popover > [slot="popup"] {
transition: opacity 100ms ease-out;
}
bwc-popover > [slot="popup"][data-starting-style],
bwc-popover > [slot="popup"][data-ending-style] {
opacity: 0;
}Closing waits for the popup's transition (duration + delay) before hiding
through the popover API; without a transition it hides synchronously.
Positioning also writes --transform-origin (the anchor point on the popup
edge) for scale/fade origins.
A [data-title] child labels the popup (aria-labelledby) and a
[data-description] child describes it (aria-describedby).
API
| Property | Attribute | Type | Default | Notes |
|---|---|---|---|---|
open |
open |
boolean |
false |
Present (or assigned) → controlled mode. Reflects to open. |
defaultOpen |
default-open |
boolean |
false |
Seed for uncontrolled mode. |
disabled |
disabled |
boolean |
false |
Disables the trigger; toggle requests are ignored. |
openOnHover |
open-on-hover |
boolean |
false |
Open on trigger hover; click toggles regardless. |
delay |
delay |
number |
0 |
Hover-open delay in ms. Non-finite values throw. |
closeDelay |
close-delay |
number |
0 |
Hover-close delay in ms. Non-finite values throw. |
modal |
modal |
boolean |
false |
Backdrop + scroll-lock; focuses the popup unless initial-focus is set. |
collisionPadding |
collision-padding |
number |
0 |
Keep this much space between popup and viewport edge, in px. Non-finite values throw. |
arrowPadding |
arrow-padding |
number |
0 |
Minimum distance between the arrow and popup edges, in px (never looser than collision-padding). Non-finite values throw. |
disableAnchorTracking |
disable-anchor-tracking |
boolean |
false |
Skip repositioning on scroll/resize/layout shifts while open. |
initialFocus |
initial-focus |
Element | string | null |
null |
Element or selector focused on open. Non-element values throw. |
finalFocus |
final-focus |
Element | string | null |
null |
Element or selector focused on close; defaults to the trigger. Non-element values throw. |
side |
side |
"top" | "right" | "bottom" | "left" |
"bottom" |
Preferred side; flips when it overflows. Invalid values throw. |
align |
align |
"start" | "center" | "end" |
"center" |
Cross-axis alignment. Invalid values throw. |
sideOffset |
side-offset |
number |
0 |
Gap in px. Non-finite values throw. |
alignOffset |
align-offset |
number |
0 |
Cross-axis shift in px. Non-finite values throw. |
strategy |
strategy |
"absolute" | "fixed" |
"fixed" |
Positioning strategy. Invalid values throw. |
anchor |
anchor |
Element | string | null |
null |
Override anchor (element or selector); null anchors to the trigger. |
triggerClass |
trigger-class |
string |
"" |
Extra classes for the trigger. |
popupClass |
popup-class |
string |
"" |
Extra classes for the popup. |
closeClass |
close-class |
string |
"" |
Extra classes for each data-close button. |
backdropClass |
backdrop-class |
string |
"" |
Extra classes for the modal backdrop. |
titleClass |
title-class |
string |
"" |
Extra classes for the data-title element. |
descriptionClass |
description-class |
string |
"" |
Extra classes for the data-description element. |
onOpenChange |
— (property only) | ((open: boolean) => void) | null |
null |
Must be a function or null. |
Methods
| Method | Notes |
|---|---|
show() |
Imperative open (needs no trigger click); anchors to the trigger unless anchor is set. |
close() |
Imperative close. |
toggle(force?) |
Toggle, or force with a boolean. |
open is the boolean state property, so imperative open is show() —
same split as HTMLDialogElement (open + show()/close()).
Events
| Name | Detail | Bubbles / composed |
|---|---|---|
open-change |
{ open: boolean } |
yes / yes |
In controlled mode native dismissals re-assert state on the next microtask
until you update open.
Slots
| Slot | Required element |
|---|---|
trigger |
exactly one button |
popup |
exactly one div |
Close buttons are any <button data-close> inside the popup. A [data-title]
child labels the popup and a [data-description] child describes it. Wrong
count or type throws TypeError. The popup gets popover="auto",
role="dialog", and data-testid="bwc-popover-popup"; parts keep author
classes plus popover-trigger / popover-popup / popover-close /
popover-title / popover-description marker classes and stable testids.
The modal backdrop gets popover-backdrop plus backdrop-class and
data-testid="bwc-popover-backdrop".
- Trigger gets
aria-haspopup="dialog",aria-controls→ popup id, andaria-expanded; state mirrors asdata-open/data-closed/data-disabledon trigger and popup. Disabled trigger uses thenot-allowedcursor. - Positioning runs through the shared floating core (
src/floating): JS measures the anchor and writesleft/topplusdata-side(placed side, after flip) /data-align; scroll/resize/layout changes reposition while open. A[data-arrow]child of the popup is centered on the cross axis automatically.
Form behavior
None.
Errors
TypeErrorfor invalid slot topology, invalidside, non-finiteside-offset, or non-functiononOpenChange.
State ownership
Without open, interactions update internal state seeded by default-open.
Assigning open permanently enters controlled mode. Property assignments and
show() / close() / toggle() apply immediately and emit open-change
when the effective state changes; other interactions only request changes in
controlled mode.