Manual UI Templates Popover

Popover

elements man ui/templates/popover

Rich content in a floating panel, opened by a trigger, that keeps the page usable.

Example
import { Popover, PopoverClose, PopoverTrigger, PopoverViewport } from "@elements/ui/popover"; import { Button } from "@elements/ui/button"; <Popover title="Dimensions" description="Set the dimensions for the layer."> <Button slot="trigger">Open popover</Button> <label>Width <input value={layer.width}></label> <label>Height <input value={layer.height}></label> </Popover> <!-- controlled: open is two-way --> <Popover open={state.filters}>...</Popover>

Parts

Part What it is
Popover The root: owns open, renders the trigger and anchor slots and, while open, the floating content.
PopoverHeader Stacks a title and description.
PopoverTitle An <h2 class="popover-title">; names the popover (aria-labelledby).
PopoverDescription A <p class="popover-description">; describes the popover (aria-describedby).
PopoverClose Wraps any element; clicking it closes this popover (not an outer one).
PopoverTrigger Wraps a trigger anywhere on the page for the Popover with the same handle. Takes handle and payload.
PopoverViewport Wraps the content; cross-fades it when the active trigger changes while open.

Attributes

Popover

Attribute Type Default Description
open boolean (two-way) false Whether the popover is open. Bind it to control it; the popover writes false back when it closes itself.
title string "" Renders a PopoverTitle in a header and names the popover.
description string "" Renders a PopoverDescription under the title.
label string "" aria-label on the content, to name a popover with no title.
side "top" | "right" | "bottom" | "left" | "inline-start" | "inline-end" "bottom" Preferred side of the trigger. It flips when there is no room. inline-start and inline-end follow the text direction (right and left under dir=rtl).
align "start" | "center" | "end" "center" Alignment along that side.
sideOffset number | ((d: OffsetData) => number) 4 Gap between the trigger and the content, in px.
alignOffset number | ((d: OffsetData) => number) 0 Shift along the alignment axis, in px.
avoidCollisions boolean true Flip and shift to stay on screen.
collisionAvoidance { side?: "flip" | "shift" | "none"; align?: "flip" | "shift" | "none"; fallbackAxisSide?: "start" | "end" | "none" } | null null How it stays in view. side "none" keeps the side with no flip, "shift" keeps the side and slides it over the trigger into view; align "flip" tries the other alignment before sliding, "shift" only slides, "none" neither; fallbackAxisSide "start" tries the inline start side of the other axis first, "end" (the default) the end side, "none" never moves it to the other axis. avoidCollisions={false} turns all of it off.
positionMethod "fixed" | "absolute" "fixed" "absolute" places the content in page coordinates instead of viewport ones.
collisionPadding number 8 Distance kept from the viewport edge, in px.
collisionBoundary string | Element | Element[] "" A selector or elements whose edges flip and shift keep it inside, besides the viewport.
arrowPadding number 6 Distance the arrow keeps from the corners, in px.
sticky "partial" | "always" "partial" "always" keeps it in the collision boundary after its anchor scrolls away.
hideWhenDetached boolean false Hide the content while the trigger is scrolled out of view.
anchor string "" CSS selector of an element to position against instead of the trigger. The anchor slot does the same for an element inside the popover's markup.
arrow boolean false Draw an arrow pointing at the trigger.
modal boolean | "trap-focus" false true: trap focus, lock page scroll, block pointer interaction outside and hide the page from assistive tech until it closes. "trap-focus": trap focus and hide the page, but the page still scrolls and a press outside still closes it.
backdrop boolean false Dim the page under the open content. A press on the backdrop closes it without reaching the page.
keepMounted boolean false Keep the closed content in the page, hidden, so its fields keep what was typed.
disableAnchorTracking boolean false Place the content once when it opens; it stays put while the page scrolls, and is placed again on a window resize.
handle string "" Names the popover for PopoverTrigger parts elsewhere on the page.
triggerId string (two-way) "" The id of the active trigger's element. Set it with open to open against that trigger; a press writes it back.
payload string (two-way) "" The payload of the PopoverTrigger that opened it, for the content to read.
openOnHover boolean false Hovering the trigger also opens it (after delay). A click pins it open.
delay number 300 With openOnHover: ms before it opens.
closeDelay number 0 With openOnHover: ms before it closes after the pointer leaves.
dismissOnEscape boolean true Escape closes it.
dismissOnOutside boolean true A press outside closes it.
initialFocus string | false "" CSS selector, inside the content, of the element to focus on open. false leaves focus where it is. Unset: the first field, or the content itself when a touch tap opened it.
returnFocus boolean true Move focus out when it closes (not after a press outside a non-modal popover).
finalFocus string "" CSS selector of the element to focus on close instead of the trigger.
size "auto" | "sm" | "md" | "lg" "md" Content width. auto fits the content.
class string "" Extra classes on the content.
onOpenChange (open: boolean, details: OpenChangeDetails) => void () => {} Called when the popover opens or closes itself, before the change. details.reason says why (trigger-press, trigger-hover, escape-key, outside-press, close-press, focus-out); details.trigger is the trigger element that caused it (undefined when no trigger did); details.cancel() keeps the current state. Not called for changes the caller makes.
onOpenChangeComplete (open: boolean) => void () => {} Called after the enter or exit animation finishes.
onEscapeKeyDown (e: KeyboardEvent) => void () => {} Called on Escape; e.preventDefault() keeps it open.
onPointerDownOutside (e: PointerEvent) => void () => {} Called on a press outside; e.preventDefault() keeps it open.
onFocusOutside (e: FocusEvent) => void () => {} Called when focus leaves; e.preventDefault() keeps it open.
onInteractOutside (e: PointerEvent | FocusEvent) => void () => {} Called after onPointerDownOutside or onFocusOutside, for either; e.preventDefault() keeps it open.
onOpenAutoFocus (e: Event) => void () => {} Called before focus moves in on open; e.preventDefault() leaves it where it is.
onCloseAutoFocus (e: Event) => void () => {} Called before focus moves out on close; e.preventDefault() leaves it alone.

PopoverHeader, PopoverTitle, PopoverDescription, PopoverViewport

Attribute Type Default Description
class string "" Extra classes.

PopoverTrigger

Attribute Type Default Description
handle string (required) The handle of the Popover it opens.
payload string "" Data handed to the popover's payload while this trigger is active.

Slots

Slot Content
trigger The element that opens the popover, usually a <Button>. It gets aria-haspopup, aria-expanded, aria-controls and data-state. With a Tooltip around it, these land on the control inside.
anchor Optional. An element to position against instead of the trigger.
default The content.

Keyboard

Keys Action
Space / Enter On the trigger: opens or closes the popover.
Tab From the trigger into the content; from the end of the content on to the page, which closes it. Trapped when modal.
Shift + Tab Moves to the previous focusable element.
Escape Closes the popover and focuses the trigger. In nested popovers, only the top one.

Data attributes

Attribute Values On Description
data-state "open" | "closed" trigger, .popover closed during the exit animation.
data-side "top" | "right" | "bottom" | "left" .popover, .popover-arrow The side it ended up on, after flipping.
data-align "start" | "center" | "end" .popover
data-modal "true" | "false" .popover
--anchor-width px .popover The trigger's width, for min-width: var(--anchor-width).
--anchor-height px .popover The trigger's height.
--available-width px .popover Room to the viewport edge across; caps the width.
--available-height px .popover Room before the viewport edge; the content (or .popover-body, with an arrow) scrolls inside it.
--popover-arrow-size length .popover Set on the content's class to size the arrow.
--transform-origin position .popover The anchor-facing edge; the enter animation scales from it.
--popup-width px .popover The content's own width while open.
--popup-height px .popover The content's own height while open.
data-state "open" | "closed" .popover-backdrop With backdrop: closed during the exit animation.
data-activation-direction "left" | "right" | "up" | "down" .popover-viewport Where the new trigger is from the old one; two values when it moved on both axes.
data-transitioning present .popover-viewport While the old content fades out over the new.
data-current / data-previous present .popover-viewport > div The live content, and a snapshot of the old one (aria-hidden, inert) while it fades.
data-popup-open present trigger While open; the same moment as the trigger's open data-state.
data-pressed present trigger While open after a press of the trigger (not a hover).
data-instant "click" | "dismiss" .popover, .popover-arrow click when a keyboard press opened it, dismiss when Escape closed it; a theme may skip the animation.
data-open / data-closed present .popover, .popover-arrow The open state as two flags; data-closed during the exit animation.
data-align "start" | "center" | "end" .popover-arrow The alignment it ended up with.
data-uncentered present .popover-arrow When the arrow cannot point at the trigger's middle (held off a corner).
data-anchor-hidden present .popover With hideWhenDetached: while the trigger is scrolled out of view.
data-starting-style / data-ending-style present .popover On the first frame of the enter animation, and through the exit animation.
--positioner-width / --positioner-height px .popover The content positions itself, so these are its own size while open.

Accessibility

  • The content has role="dialog", named by its title (aria-labelledby) or by label (aria-label), and described by its description (aria-describedby).
  • The trigger gets aria-haspopup="dialog", aria-expanded and aria-controls.
  • Focus moves into the content when it opens and back to the trigger on Escape or a close button.
  • The content follows the trigger in the DOM, so Tab order runs trigger, content, rest of the page, with no portal.
  • With several triggers, only the active one has aria-expanded="true"; each has aria-haspopup and aria-controls.
  • modal adds aria-modal="true", a focus trap, scroll lock, blocks pointer input outside and sets aria-hidden on the rest of the page, so only the content is read.

See also