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>.

Multiple triggers within the Popover
<Popover>
  <PopoverTrigger>Trigger 1</PopoverTrigger>
  <PopoverTrigger>Trigger 2</PopoverTrigger>
  ...
</Popover>
Multiple detached triggers
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.