Popover
elements man ui/templates/popoverRich content in a floating panel, opened by a trigger, that keeps the page usable.
Exampleimport { 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
- Button (opens in a new tab): A button or a link that looks like one, in six variants and four sizes.
- HoverCard (opens in a new tab): A preview of what a link points to, shown while the pointer rests on it.
- Tooltip (opens in a new tab): A short label for a control, shown on hover and keyboard focus.