<ui-popover>

ui-popover.mjs · Full test suite

ElementAttributesPropertiesMethodsEvents
<ui-popover> placement, label .placement, .label, .open .close() ui-popover-toggle (detail.open)

.open is read-only and reports whether the panel is showing. .close() dismisses it and is safe to call when it is already closed. There is no .open() method: the trigger button carries popovertarget, so opening is the UA's job. ui-popover-toggle fires on the <ui-popover> element itself and does not bubble — listen on the element, never on an ancestor.

Accessibility (WCAG)

Handling Escape

The panel's keydown listener handles Escape with both stopPropagation() and preventDefault(). The first blocks ancestor listeners; the second prevents the browser from closing another overlay after this panel closes. Both calls are required.

If defaultPrevented is already true, the panel leaves the event alone. An inner picker, menu, or combobox has already handled it and should close without closing the panel too. The next Escape press can then close the panel. Nested popovers stop propagation themselves, so their handled events do not reach this listener.

stopPropagation() prevents an ancestor panel from handling the same Escape event and closing too when nesting popovers. If an inner widget has already cancelled the event, this panel leaves it alone, allowing it to continue bubbling with defaultPrevented set. An inner popover stops its own handled events, so those do not reach the outer listener.

preventDefault() prevents a second close by the browser. Because the panel uses popover="auto", it participates in the browser's close-watcher stack. The browser processes Escape's close request after event dispatch, when the listener has already removed this panel from the stack. Without cancellation, the request would close the next overlay, such as the containing dialog. stopPropagation() alone cannot prevent that.

Capture listeners run before the panel's handler and therefore see defaultPrevented as false. At that point, check document.querySelector(':popover-open') to detect the still-open panel; cancellation has not happened yet.

Menus cancel Escape but allow it to bubble after closing. Popovers cancel and stop events they handle; events already handled by an inner widget can still bubble through them. Ancestor Escape handlers should therefore check defaultPrevented first. See Accessibility for the differences between overlay components.

Test with real key presses in a browser. A synthetic KeyboardEvent does not create a browser close request, and a <dialog open> stand-in has no close watcher. Neither can reveal an unintended second close.

1. Basic

Simple popover with text content. The first direct-child <button> becomes the trigger; everything else is moved into the panel.

Show code

2. Form content

Settings popover with form controls — icon button as trigger.

Show code

3. Icon button + rich content

Popover with keyboard shortcuts — icon button as trigger, definition list as content.

Show code

4. Placement

Eight placement options via the placement attribute — four on the block axis (above and below the trigger) and four on the inline axis (beside it). Default is bottom-end. The resting gap that separates panel from trigger sits on whichever edge faces the trigger, so it follows the placement rather than always being a top margin.

Show code

Side placements use position-try-fallbacks: flip-inline to move to the other side when needed near a viewport edge. The four block-axis placements do not flip. §7 explains their available-space sizing, which automatic flipping would obscure.

5. Nesting one panel inside another

A <ui-popover> may sit inside another one's content and open as a flyout off a row of it — the account menu's Advanced section is built this way. Nothing has to be wired for it: the panel is appended to its own host element, so a nested instance is a DOM descendant of the outer panel, and the Popover API computes its light-dismiss ancestry from that containment. Opening the inner panel therefore leaves the outer one showing, and closing the outer one takes the inner with it.

Use a side placement so the nested panel opens beside its parent. Escape should close one level and return focus to that level's trigger. Follow the Escape-handling rules: stopping propagation protects the parent listener, while cancellation prevents the browser's close watcher from closing the parent after the child.

Show code

A <ui-menu> is not the way to do this when the flyout holds anything but action rows. That component is APG role="menu", which forbids static text and embedded widgets — the same reason an account menu is a popover in the first place. Reach for <ui-menu>'s own submenus only for pure action lists.

6. Multiple popovers

Two popovers next to each other — opening one automatically closes the other (popover="auto").

Show code

7. Sizing

The panel uses its content's preferred width, capped at 45 ch (--size-content-2), with longer text wrapping. Both width declarations are necessary. The browser's default fit-content depends on the space between the inset and viewport edge, which can make a panel unexpectedly narrow. Without the explicit width, the 41-character paragraph in §1 measured 337×90 mid-page but 96×210 near the edge in both engines. Conversely, max-content without a cap would keep a long paragraph on one line. Check the width and cap together.

This block-placement behavior is visible in both engines. Unlike <ui-menu> and tooltip(), it has no position-try-fallbacks that could move the panel to a roomier side.

Declare a width of your own when the panel should be a fixed size whatever it holds — any width or min-inline-size on your own selector outranks the component's. The studio's bookmark form takes width: 16rem, the account menu min-inline-size: 15rem.

Show code