Jone

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:

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

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

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

Detached triggers with payload
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

NameTypeDefaultDescription
defaultOpenOptionalboolean | undefinedfalseWhether the popover is initially open. To render a controlled popover, use the `open` prop instead.
openOptionalboolean | undefinedWhether the popover is currently open.
onOpenChangeCompleteOptional((open: boolean) => void) | undefinedEvent handler called after any animations complete when the popover is opened or closed.
actionsRefOptionalReact.RefObject<PopoverRoot.Actions | null> | undefinedA 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.
modalOptionalboolean | 'trap-focus' | undefinedfalseDetermines 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.
triggerIdOptionalstring | null | undefinedID 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).
defaultTriggerIdOptionalstring | null | undefinedID of the trigger that the popover is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open popover.
handleOptionalPopoverHandle<Payload> | undefinedA handle to associate the popover with a trigger. If specified, allows external triggers to control the popover's open state.
childrenOptionalReact.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

NameTypeDefaultDescription
keepMountedOptionalboolean | undefinedfalseWhether to keep the portal mounted in the DOM while the popup is hidden.

On this page