<bmx-popover>
A small panel of content hung off a control: a filter, a colour picker, a profile card, a form of two fields. Opened by a press, focus moves into it, and the page behind it stays live.
9 properties · 1 events · 2 methods · 6 parts
Example
Amara Okonkwo
Account manager since 2023. Handles the Northwind and Contoso accounts. Open the full profile
aria-describedby. Anything a person has to reach belongs here,
where focus goes in and comes back. A “tooltip” holding a link is content a keyboard user can see and cannot get to,
and it is the commonest accessibility defect in commercial component suites.
Show markup
<div class="row">
<bmx-button id="ex-pop-filters" variant="outline" tone="neutral">Filters</bmx-button>
<bmx-popover for="#ex-pop-filters" label="Filters">
<div style="display: flex; flex-direction: column; gap: 8px">
<bmx-checkbox label="Only mine" checked="true"></bmx-checkbox>
<bmx-checkbox label="Archived"></bmx-checkbox>
<bmx-checkbox label="Shared with me"></bmx-checkbox>
</div>
<div slot="footer">
<bmx-button id="ex-pop-apply" size="sm">Apply</bmx-button>
</div>
</bmx-popover>
<bmx-button id="ex-pop-profile" variant="outline" tone="neutral">Who is this?</bmx-button>
<bmx-popover for="#ex-pop-profile" placement="bottom-start">
<h4 slot="header" style="margin: 0">Amara Okonkwo</h4>
<p style="margin: 0">
Account manager since 2023. Handles the Northwind and Contoso accounts.
<a href="#bmx-popover">Open the full profile</a>
</p>
</bmx-popover>
</div>
<div class="row">
<span class="note" id="ex-pop-out">Open one and press Tab — the panel is a dialog, and focus is inside it.</span>
</div>
<div class="row" style="margin-block-start: 1rem">
<span class="note">
That link is why this is a popover and not a tooltip. A tooltip <em>describes</em> its anchor — a few words, no
focus, reaching a screen reader through <code>aria-describedby</code>. Anything a person has to reach belongs here,
where focus goes in and comes back. A “tooltip” holding a link is content a keyboard user can see and cannot get to,
and it is the commonest accessibility defect in commercial component suites.
</span>
</div>
<script type="module">
await customElements.whenDefined('bmx-popover');
const out = document.getElementById('ex-pop-out');
/*
* The event says why it closed, which is the part a consumer needs: a panel
* dismissed with Escape has been rejected, and one closed by its Apply button
* has been accepted. Treating the two the same is how a filter panel ends up
* applying a filter the user backed out of.
*/
document.querySelector('bmx-popover').addEventListener('bmxToggle', event => {
const { open, reason } = event.detail;
out.textContent = open ? 'Open — focus is on the first checkbox.' : `Closed (${reason}).`;
});
document.getElementById('ex-pop-apply').addEventListener('bmxActivate', () => {
document.querySelector('bmx-popover').closePopover();
});
</script>
<bmx-button id="filters">Filters</bmx-button>
<bmx-popover for="#filters" label="Filters">
<bmx-checkbox>Only mine</bmx-checkbox>
<bmx-button size="sm">Apply</bmx-button>
</bmx-popover>
A POPOVER CONTAINS; A TOOLTIP DESCRIBES
The distinction the pair turns on, and the reason there are two components
rather than one with a flag. This is a dialog: it has an accessible name,
focus goes into it, Escape closes it and focus comes back. A bmx-tooltip is
a description of its anchor - a few words, no focus, reaching a screen
reader through aria-describedby. Anything a user has to reach belongs here.
A "tooltip" holding a link is the single most common accessibility defect in
commercial component suites, and it is this component that it should have
been.
WHY IT IS NOT bmx-dialog
A dialog is modal: the rest of the page goes inert, the scroll is locked, and
the user must deal with it. A popover is non-modal - the page behind stays
usable, nothing is locked, and it closes the moment attention goes elsewhere.
So it takes none of bmx-dialog's machinery except the four-phase close, and
it adds the one thing a modal never needs: closing because focus left. A
non-modal panel that stays open behind the user's focus is a panel that
covers whatever they Tab to next.
WHY THERE IS ALWAYS A CLOSE BUTTON
Escape is the keyboard's way out and an outside press is the pointer's, but
neither is discoverable and one of them is unavailable to a person using a
touch screen with a switch device. A visible control that says what it does
is the only way out that needs no prior knowledge. Style it away if a design
demands it - part="close" is there - but it is drawn by default because the
default should be the accessible one.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
closeLabel |
close-label |
string |
'Close' |
The close button's accessible name. |
closeOnFocusLeave |
close-on-focus-leave |
boolean |
true |
Whether it closes when focus leaves it. On, and it is what makes a non-modal panel behave: one that stays open behind the user's focus is a panel covering whatever they Tab to next. Turn it off for a panel that deliberately outlives the interaction - a pinned inspector - and give the user another way out. |
disabled |
disabled |
boolean |
false |
Nothing opens it. |
distance |
distance |
number |
8 |
Gap between the trigger and the panel, in pixels. |
for |
for |
string | HTMLElement |
— | What opens it: a selector, or the element itself. Left off, it is the element immediately before this one. The trigger's aria-haspopup, aria-expanded and aria-controls are wired for you, and a press toggles the panel - so the whole widget is one attribute and no script, exactly as bmx-menu's trigger is. |
label |
label |
string |
— | The panel's accessible name. A dialog without one is announced as "dialog" and nothing else. When there is a header slot its text is used instead, which is usually what an author meant. |
open |
open |
boolean |
false |
Whether it is open. |
placement |
placement |
BmxPlacement |
'bottom-start' |
Where it prefers to sit. It flips when there is no room. |
returnFocus |
return-focus |
boolean |
true |
Whether closing returns focus to the trigger. |
Events
| Event | Detail | Description |
|---|---|---|
bmxToggle |
BmxPopoverToggleDetail |
Fired when it opens or closes, saying which and why. |
Methods
| Method | Signature | Description |
|---|---|---|
closePopover |
closePopover() => Promise<void> |
Close it. |
openPopover |
openPopover() => Promise<void> |
Open it. Named for the component, not for the platform's showPopover. |
Slots
| Slot | Description |
|---|---|
(default) |
The panel's content. |
footer |
Actions along the bottom. |
header |
A heading. Names the dialog when label is not given. |
CSS shadow parts
| Part | Description |
|---|---|
arrow |
The pointer drawn against the trigger. |
close |
The close button. |
content |
The body. |
footer |
The actions row. |
header |
The heading row. |
surface |
The floating panel. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-popover-arrow-size |
The pointer's size. Set it to 0 for no pointer at all. |
--bmx-popover-background |
The panel's background. |
--bmx-popover-border-color |
Its border. |
--bmx-popover-color |
Its text colour. |
--bmx-popover-gap |
Space between the header, the body and the footer. |
--bmx-popover-inline-size |
How wide the panel is. |
--bmx-popover-max-block-size |
How tall it may grow before the body scrolls. |
--bmx-popover-padding |
Space inside the body. |
--bmx-popover-radius |
Its corner radius. |
--bmx-popover-shadow |
The shadow under it. |