Popover
A non-modal popup anchored to a trigger, positioned with flip/shift and an optional arrow.
Installation
npx nuecrea-ui add popoverUsage
import { Button } from '@/components/ui/button';
import * as Popover from '@/components/ui/popover';
function Example({
label,
side,
align,
arrowPadding,
disabled,
}: {
label: string;
side?: 'top' | 'bottom' | 'left' | 'right';
align?: 'start' | 'center' | 'end';
arrowPadding?: number;
disabled?: boolean;
}) {
return (
<Popover.Root>
<Popover.Trigger
disabled={disabled}
render={<Button variant="outline" />}
>
{label}
</Popover.Trigger>
<Popover.Portal>
<Popover.Positioner
side={side}
align={align}
arrowPadding={arrowPadding}
>
<Popover.Popup>
<Popover.Arrow />
<Popover.Title>{label}</Popover.Title>
<Popover.Description>
You're all caught up.
</Popover.Description>
<div className="mt-3 flex justify-end">
<Popover.Close render={<Button size="sm" variant="outline" />}>
Dismiss
</Popover.Close>
</div>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>
);
}
export default function PopoverStory() {
return (
<main className="relative h-[520px]">
{/* Pushed against the left edge with a large arrowPadding: the arrow can't reach the
trigger centre (data-uncentered). */}
<div className="absolute top-12 left-0">
<Example label="Help" arrowPadding={110} />
</div>
<div className="absolute top-0 left-[600px]">
<Example label="Archived" disabled />
</div>
{/* Near the left edge: the popup shifts and the arrow is uncentered. */}
<div className="absolute top-0 left-0">
<Example label="Notifications" />
</div>
<div className="absolute top-40 left-60">
<Example label="Share" side="right" align="start" />
</div>
{/* Near the bottom edge: side="bottom" flips to top. */}
<div className="absolute bottom-0 left-60">
<Example label="Filters" />
</div>
{/* Room to scroll a trigger out of view (data-anchor-hidden). */}
<div aria-hidden="true" className="absolute top-full h-[600px] w-px" />
</main>
);
}API reference
Popover.Root
Owns the open state. Renders nothing.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state. |
defaultOpen | boolean | false | Initial open state. |
modal | boolean | 'trap-focus' | false | true: trap focus, inert page, lock scroll. |
| Event | Payload | Description |
|---|---|---|
onOpenChange | boolean | Fired when it opens or closes. Angular: the `open` model's change event. |
onOpenChangeComplete | boolean | Fired after the animation finishes. |
Popover.Trigger
Toggles the popover. Renders <button>.
| Data attribute | Present when |
|---|---|
data-popup-open | open |
data-pressed | open after a pointer press |
data-disabled | disabled |
Popover.Portal
Moves the popup to the end of <body>. Renders <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
keepMounted | boolean | false | Keep in the DOM while closed. |
Popover.Positioner
Positions the popup (transform) and exposes size variables. Renders <div> with role presentation.
| Prop | Type | Default | Description |
|---|---|---|---|
side | 'top' | 'bottom' | 'left' | 'right' | "bottom" | Preferred side; flips on collision. |
align | 'start' | 'center' | 'end' | "center" | Alignment along the side. |
sideOffset | number | 0 | Gap from the trigger (styled default 8). |
alignOffset | number | 0 | Shift along the alignment axis. |
arrowPadding | number | 5 | Minimum distance of the arrow from the popup edges. |
collisionPadding | number | 5 | Distance to keep from the viewport edges. |
| Data attribute | Present when |
|---|---|
data-open | open |
data-closed | closed / animating out |
data-anchor-hidden | the trigger is scrolled out of view |
data-align | always: resolved alignment |
data-side | always: resolved side after flipping |
data-instant | opened from the keyboard: value "click" (skip the enter animation if wanted) |
| CSS variable | Description |
|---|---|
--available-width | Space between trigger and viewport edge |
--available-height | Space between trigger and viewport edge |
--anchor-width | Trigger width |
--anchor-height | Trigger height |
--transform-origin | Arrow (or trigger centre) position, for scale animations |
Popover.Popup
The popover (role=dialog). Focus moves in on open; Tab out of it closes it. Renders <div> with role dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
initialFocus | boolean | HTMLElement | null | — | Element focused on open; defaults to the first tabbable. |
finalFocus | boolean | HTMLElement | null | — | Element focused on close; defaults to the trigger. |
| Data attribute | Present when |
|---|---|
data-open | open |
data-closed | closed / animating out |
data-align | always: resolved alignment |
data-side | always: resolved side after flipping |
data-instant | opened from the keyboard: value "click" (skip the enter animation if wanted) |
data-starting-style | animating in |
data-ending-style | animating out |
Popover.Arrow
Points at the trigger. Renders <div>.
| Data attribute | Present when |
|---|---|
data-open | open |
data-closed | closed / animating out |
data-uncentered | the arrow can't reach the trigger centre (clamped by arrowPadding) |
data-align | always: resolved alignment |
data-side | always: resolved side after flipping |
Popover.Title
Labels the popup. Renders <h2>.
Popover.Description
Describes the popup. Renders <p>.
Popover.Close
Closes the popover. Renders <button>.
Keyboard
| Key | Action |
|---|---|
| Enter / Space | On the trigger: toggle. |
| Escape | Close and return focus to the trigger. |
| Tab | From the last element: close and continue to the element after the trigger. |
| Shift+Tab | From the first element: close and return to the trigger. |