Popover
An accessible popup anchored to a button.
Installation
pnpm dlx shadcn@latest add @lumi-ui/popover
Basic Usage
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverTitle,
PopoverTrigger,
} from "@/registry/ui/popover";
export function PopoverDemo() {
return (
<Popover>
<PopoverTrigger>Open</PopoverTrigger>
<PopoverContent>
<PopoverTitle>Popover title</PopoverTitle>
<PopoverDescription>Popover description</PopoverDescription>
</PopoverContent>
</Popover>
);
}Anatomy
<Popover>
<PopoverTrigger />
<PopoverContent>
<PopoverTitle />
<PopoverDescription />
</PopoverContent>
</Popover>Features
With arrow
Match anchor width
Cookbook
Open on hover
Detached triggers
import { createPopoverHandle } from "@/components/ui/popover";
const demoPopover = createPopoverHandle();
<PopoverTrigger handle={demoPopover}>
Trigger
</PopoverTrigger>
<Popover handle={demoPopover}>
...
</Popover>Multiple triggers
A single popover can be opened by multiple trigger elements. You can achieve this by using the same handle for several detached triggers, or by placing multiple <PopoverTrigger> components inside a single <Popover>.
<Popover>
<PopoverTrigger>Trigger 1</PopoverTrigger>
<PopoverTrigger>Trigger 2</PopoverTrigger>
...
</Popover>const demoPopover = createPopoverHandle();
<PopoverTrigger handle={demoPopover}>
Trigger 1
</PopoverTrigger>
<PopoverTrigger handle={demoPopover}>
Trigger 2
</PopoverTrigger>
<Popover handle={demoPopover}>
...
</Popover>The popover can render different content depending on which trigger opened it. This is achieved by passing a payload to the <PopoverTrigger> and using the function-as-a-child pattern in <Popover>. The payload can be strongly typed by providing a type argument to the createPopoverHandle() function:
const demoPopover = createPopoverHandle<{ text: string }>();
<PopoverTrigger handle={demoPopover} payload={{ text: 'Trigger 1' }}>
Trigger 1
</PopoverTrigger>
<PopoverTrigger handle={demoPopover} payload={{ text: 'Trigger 2' }}>
Trigger 2
</PopoverTrigger>
<Popover handle={demoPopover}>
{({ payload }) => (
<PopoverPortal>
<PopoverPositioner>
<PopoverPopup>
<PopoverArrow />
<PopoverTitle>Popover</PopoverTitle>
{payload !== undefined && (
<PopoverDescription>
This has been opened by {payload.text}
</PopoverDescription>
)}
</PopoverPopup>
</PopoverPositioner>
</PopoverPortal>
)}
</Popover>Controlled mode with multiple triggers
You can control the popover’s open state externally using the open and onOpenChange props on <Popover>. This allows you to manage the popover’s visibility based on your application’s state. When using multiple triggers, you have to manage which trigger is active with the triggerId prop on <Popover> and the id prop on each <PopoverTrigger>.
Note that there is no separate onTriggerIdChange prop. Instead, the onOpenChange callback receives an additional argument, eventDetails, which contains the trigger element that initiated the state change.
Animating the Popover
You can animate a popover as it moves between different trigger elements. This includes animating its position, size, and content.
Position and Size
To animate the popover’s position, apply CSS transitions to the left, right, top, and bottom properties of the Positioner part. To animate its size, transition the width and height of the Popup part.
Content
The popover also supports content transitions. This is useful when different triggers display different content within the same popover.
To enable content animations, wrap the content in the <PopoverViewport> part. This part provides features to create direction-aware animations. It renders a div with a data-activation-direction attribute (left, right, up, or down) that indicates the new trigger’s position relative to the previous one.
Inside the <PopoverViewport>, the content is further wrapped in divs with data attributes to help with styling:
data-current: The currently visible content when no transitions are present or the incoming content.data-previous: The outgoing content during a transition.
You can use these attributes to style the enter and exit animations.