v1.0.0

<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

Filters
Apply
Who is this?

Amara Okonkwo

Account manager since 2023. Handles the Northwind and Contoso accounts. Open the full profile

Open one and press Tab — the panel is a dialog, and focus is inside it.
That link is why this is a popover and not a tooltip. A tooltip describes its anchor — a few words, no focus, reaching a screen reader through 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

PropertyAttributeTypeDefaultDescription
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

EventDetailDescription
bmxToggle BmxPopoverToggleDetail Fired when it opens or closes, saying which and why.

Methods

MethodSignatureDescription
closePopover closePopover() => Promise<void> Close it.
openPopover openPopover() => Promise<void> Open it. Named for the component, not for the platform's showPopover.

Slots

SlotDescription
(default) The panel's content.
footer Actions along the bottom.
header A heading. Names the dialog when label is not given.

CSS shadow parts

PartDescription
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

PropertyDescription
--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.