Popover
A high-quality, unstyled React popover component that displays an accessible popup anchored to a button.
An accessible popup anchored to a button.
Anatomy
Import the component and assemble its parts:
import { Popover } from '@base-ui/react/popover';
<Popover.Root>
<Popover.Trigger />
<Popover.Portal>
<Popover.Backdrop />
<Popover.Positioner>
<Popover.Popup>
<Popover.Arrow />
<Popover.Viewport>
<Popover.Title />
<Popover.Description />
<Popover.Close />
</Popover.Viewport>
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
</Popover.Root>;Examples
Opening on hover
This example shows how you can configure the popover to open on hover using the openOnHover prop.
You can use the delay prop to specify how long to wait (in milliseconds) before the popover opens on hover.
Detached triggers
A popover can be controlled by a trigger located either inside or outside the <Popover.Root> component.
For simple, one-off interactions, place the <Popover.Trigger> inside <Popover.Root>, as shown in the example at the top of this page.
However, if defining the popover's content next to its trigger is not practical, you can use a detached trigger.
This involves placing the <Popover.Trigger> outside of <Popover.Root> and linking them with a handle created by the Popover.createHandle() function.
const demoPopover = Popover.createHandle();
<Popover.Trigger handle={demoPopover}>
Trigger
</Popover.Trigger>
<Popover.Root handle={demoPopover}>
...
</Popover.Root>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 <Popover.Trigger> components inside a single <Popover.Root>.
<Popover.Root>
<Popover.Trigger>Trigger 1</Popover.Trigger>
<Popover.Trigger>Trigger 2</Popover.Trigger>
...
</Popover.Root>const demoPopover = Popover.createHandle();
<Popover.Trigger handle={demoPopover}>
Trigger 1
</Popover.Trigger>
<Popover.Trigger handle={demoPopover}>
Trigger 2
</Popover.Trigger>
<Popover.Root handle={demoPopover}>
...
</Popover.Root>The popover can render different content depending on which trigger opened it.
This is achieved by passing a payload to the <Popover.Trigger> and using the function-as-a-child pattern in <Popover.Root>.
The payload can be strongly typed by providing a type argument to the createHandle() function:
const demoPopover = Popover.createHandle<{ text: string }>();
<Popover.Trigger handle={demoPopover} payload={{ text: 'Trigger 1' }}>
Trigger 1
</Popover.Trigger>
<Popover.Trigger handle={demoPopover} payload={{ text: 'Trigger 2' }}>
Trigger 2
</Popover.Trigger>
<Popover.Root handle={demoPopover}>
{({ payload }) => (
<Popover.Portal>
<Popover.Positioner sideOffset={8}>
<Popover.Popup className={styles.Popup}>
<Popover.Arrow className={styles.Arrow}>
<ArrowSvg />
</Popover.Arrow>
<Popover.Title className={styles.Title}>Popover</Popover.Title>
{payload !== undefined && (
<Popover.Description className={styles.Description}>
This has been opened by {payload.text}
</Popover.Description>
)}
</Popover.Popup>
</Popover.Positioner>
</Popover.Portal>
)}
</Popover.Root>Controlled mode with multiple triggers
You can control the popover's open state externally using the open and onOpenChange props on <Popover.Root>.
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.Root> and the id prop on each <Popover.Trigger>.
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 <Popover.Viewport> 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 <Popover.Viewport>, 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.
API reference
Root
| Name | Type | Default | Description |
|---|---|---|---|
defaultOpenOptional | boolean | undefined | false | Whether the popover is initially open. To render a controlled popover, use the `open` prop instead. |
openOptional | boolean | undefined | — | Whether the popover is currently open. |
onOpenChangeCompleteOptional | ((open: boolean) => void) | undefined | — | Event handler called after any animations complete when the popover is opened or closed. |
actionsRefOptional | React.RefObject<PopoverRoot.Actions | null> | undefined | — | A ref to imperative actions. - `unmount`: When specified, the popover will not be unmounted when closed. Instead, the `unmount` function must be called to unmount the popover manually. Useful when the popover's animation is controlled by an external library. - `close`: Closes the dialog imperatively when called. |
modalOptional | boolean | 'trap-focus' | undefined | false | Determines if the popover enters a modal state when open. - `true`: user interaction is limited to the popover: document page scroll is locked, and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. - `'trap-focus'`: focus is trapped inside the popover, but document page scroll is not locked and pointer interactions outside of it remain enabled. |
triggerIdOptional | string | null | undefined | — | ID of the trigger that the popover is associated with. This is useful in conjunction with the `open` prop to create a controlled popover. There's no need to specify this prop when the popover is uncontrolled (i.e. when the `open` prop is not set). |
defaultTriggerIdOptional | string | null | undefined | — | ID of the trigger that the popover is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open popover. |
handleOptional | PopoverHandle<Payload> | undefined | — | A handle to associate the popover with a trigger. If specified, allows external triggers to control the popover's open state. |
childrenOptional | React.ReactNode | PayloadChildRenderFunction<Payload> | — | The content of the popover. This can be a regular React node or a render function that receives the `payload` of the active trigger. |
Portal
| Name | Type | Default | Description |
|---|---|---|---|
keepMountedOptional | boolean | undefined | false | Whether to keep the portal mounted in the DOM while the popup is hidden. |