---
id: "packages/active-ui/popover"
title: "Popover"
description: "Content anchored to the element that opened it, such as a small form or an explanation, without leaving the page."
url: "/packages/active-ui/popover"
locale: "en"
revision: "9e171d82f61af996"
---

# Popover

A popover opens next to its trigger with content the reader can interact with: a short form that
edits one value, a set of options, an explanation. The page behind stays usable, unlike behind a
[dialog](/packages/active-ui/dialog). For a label that only names an icon button, use a
[tooltip](/packages/active-ui/tooltip); for a preview of a link, a
[hover card](/packages/active-ui/hover-card).

**Example: Edit in place, and an explanation**

Client: Runs entirely in your browser. Source: `examples/ui/popover.tsx`.

```tsx
// apps/docs/examples/ui/popover.tsx
'use client';

import {useState} from 'react';
import {InfoIcon} from 'lucide-react';

import {Button} from '@workspace/active-ui/components/primitives/button';
import {
  Field,
  FieldLabel,
} from '@workspace/active-ui/components/primitives/field';
import {Input} from '@workspace/active-ui/components/primitives/input';
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from '@workspace/active-ui/components/primitives/popover';

/**
 * Content anchored to the element that opened it: a short form that edits
 * one value in place, and an explanation behind an icon button. The page
 * stays usable; a click outside or Escape closes the popover.
 */
export default function PopoverExample() {
  const [open, setOpen] = useState(false);
  const [rate, setRate] = useState('7.5');
  const [draft, setDraft] = useState(rate);
  return (
    <div className="flex flex-wrap items-center justify-center gap-4">
      <Popover
        open={open}
        onOpenChange={(next) => {
          setOpen(next);
          if (next) setDraft(rate);
        }}
      >
        <PopoverTrigger render={<Button variant="outline" />}>
          Contribution rate: {rate}%
        </PopoverTrigger>
        <PopoverContent className="w-72">
          <PopoverHeader>
            <PopoverTitle>Contribution rate</PopoverTitle>
            <PopoverDescription>
              The share of the salary paid into the scheme.
            </PopoverDescription>
          </PopoverHeader>
          <form
            className="mt-3 flex items-end gap-2"
            onSubmit={(event) => {
              event.preventDefault();
              setRate(draft);
              setOpen(false);
            }}
          >
            <Field>
              <FieldLabel htmlFor="popover-rate">Rate in %</FieldLabel>
              <Input
                id="popover-rate"
                inputMode="decimal"
                value={draft}
                onChange={(event) => setDraft(event.target.value)}
              />
            </Field>
            <Button type="submit">Save</Button>
          </form>
        </PopoverContent>
      </Popover>

      <Popover>
        <PopoverTrigger
          render={
            <Button
              variant="ghost"
              size="icon"
              aria-label="About the waiting period"
            />
          }
        >
          <InfoIcon aria-hidden />
        </PopoverTrigger>
        <PopoverContent side="right" className="w-64">
          <PopoverHeader>
            <PopoverTitle>Waiting period</PopoverTitle>
            <PopoverDescription>
              New employees join the scheme after two months of service.
            </PopoverDescription>
          </PopoverHeader>
        </PopoverContent>
      </Popover>
    </div>
  );
}
```

## Usage

```tsx
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from '@workspace/active-ui/components/primitives/popover';

<Popover>
  <PopoverTrigger render={<Button variant="outline" />}>Rate</PopoverTrigger>
  <PopoverContent>
    <PopoverHeader>
      <PopoverTitle>Contribution rate</PopoverTitle>
      <PopoverDescription>The share of the salary paid in.</PopoverDescription>
    </PopoverHeader>
  </PopoverContent>
</Popover>;
```

## Anatomy

- `Popover`: the open state; `open` and `onOpenChange`, or uncontrolled. `modal` (default
  `false`) makes the page inert while it is open.
- `PopoverTrigger`: the element that opens it on click; `openOnHover` opens it on hover as well.
- `PopoverContent`: the panel, placed by `side` (default `bottom`), `align` (default `center`)
  and their offsets. It flips and shifts to stay inside the viewport. It is `18rem` wide by default;
  set a width with `className`.
- `PopoverHeader` with `PopoverTitle` and `PopoverDescription`, which name and describe the
  panel.

## What your application provides

- The content and, for a form inside it, what saving does. Close the popover after saving through
  the open state, as the example does.
- An accessible name for an icon-only trigger.

## Accessibility and keyboard

- The trigger is a button with `aria-expanded`; `Enter` or `Space` opens the popover and moves the
  focus into it.
- `Escape` and a click outside close it; the focus returns to the trigger.
- The panel is a `role="dialog"` named by its title and described by its description.

## API

### PopoverContent

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `align` | `Align` | no | How to align the popup relative to the specified side. |
| `alignOffset` | `number \| OffsetFunction` | no | Additional offset along the alignment axis in pixels. Also accepts a function that returns the offset to read the dimensions of the anchor and positioner elements, along with its side and alignment.  The function takes a `data` object parameter with the following properties: - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`. - `data.side`: which side of the anchor element the positioner is aligned against. - `data.align`: how the positioner is aligned relative to the specified side. |
| `finalFocus` | `boolean \| RefObject<HTMLElement \| null> \| ((closeType: InteractionType) => void \| boolean \| HTMLElement \| null)` | no | Determines the element to focus when the popover is closed.  - `false`: Do not move focus. - `true`: Move focus based on the default behavior (trigger or previously focused element). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, `null` to fall back to the default behavior, or `false`/`undefined` to do nothing. |
| `initialFocus` | `boolean \| RefObject<HTMLElement \| null> \| ((openType: InteractionType) => void \| boolean \| HTMLElement \| null)` | no | Determines the element to focus when the popover is opened. By default, focus moves to the first tabbable element inside the popup, except when the popover is opened by touch — then the popup itself is focused to avoid opening the virtual keyboard.  - `false`: Do not move focus. - `true`: Move focus based on the default behavior (first tabbable element or popup). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, `null` to fall back to the default behavior, or `false`/`undefined` to do nothing. |
| `render` | `ComponentRenderFn<HTMLProps, PopoverPopupState> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |
| `side` | `Side` | no | Which side of the anchor element to align the popup against. May automatically change to avoid collisions. |
| `sideOffset` | `number \| OffsetFunction` | no | Distance between the anchor and the popup in pixels. Also accepts a function that returns the distance to read the dimensions of the anchor and positioner elements, along with its side and alignment.  The function takes a `data` object parameter with the following properties: - `data.anchor`: the dimensions of the anchor element with properties `width` and `height`. - `data.positioner`: the dimensions of the positioner element with properties `width` and `height`. - `data.side`: which side of the anchor element the positioner is aligned against. - `data.align`: how the positioner is aligned relative to the specified side. |

### Other parts

**Popover**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `actionsRef` | `RefObject<PopoverRootActions \| null>` | no | A ref to imperative actions. - `unmount`: Ends the closing phase of the popover after an externally controlled closing animation finishes. Call `preventUnmountOnClose()` in `onOpenChange` first, otherwise the popover completes closing on its own. Whether it leaves the DOM is decided by `keepMounted` on the portal. - `close`: Closes the popover imperatively when called. |
| `children` | `PayloadChildRenderFunction<unknown> \| ReactNode` | no | The content of the popover. This can be a regular React node or a render function that receives the `payload` of the active trigger. |
| `defaultOpen` | `boolean` | no | Whether the popover is initially open.  To render a controlled popover, use the `open` prop instead. |
| `defaultTriggerId` | `string \| null` | no | 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. |
| `handle` | `PopoverHandle<unknown>` | no | A handle to associate the popover with a trigger. If specified, allows external triggers to control the popover's open state. |
| `modal` | `"trap-focus" \| boolean` | no | 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.  On touch devices, a `true` modal blocks outside taps but leaves the page scrollable unless the popup spans nearly the full viewport width, matching native iOS behavior.  When `modal` is `true`, focus trapping is enabled only if `<Popover.Close>` is rendered inside `<Popover.Popup>`. It can be visually hidden with your own CSS if needed, such as Tailwind's `sr-only` utility.  When `modal` is `'trap-focus'`, render `<Popover.Close>` inside `<Popover.Popup>` so touch screen readers can escape the popup. |
| `onOpenChange` | `((open: boolean, eventDetails: PopoverRoot.ChangeEventDetails) => void)` | no | Event handler called when the popover is opened or closed. |
| `onOpenChangeComplete` | `((open: boolean) => void)` | no | Event handler called after any animations complete when the popover is opened or closed. |
| `open` | `boolean` | no | Whether the popover is currently open. |
| `triggerId` | `string \| null` | no | 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 (that is, when the `open` prop is not set). |

**PopoverDescription**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `render` | `ComponentRenderFn<HTMLProps, PopoverDescriptionState> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**PopoverHeader**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |

PopoverHeader has no props of its own.

**PopoverTitle**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `render` | `ComponentRenderFn<HTMLProps, PopoverTitleState> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

**PopoverTrigger**

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `closeDelay` | `number` | no | How long to wait before closing the popover that was opened on hover. Specified in milliseconds.  Requires the `openOnHover` prop. |
| `delay` | `number` | no | How long to wait before the popover may be opened on hover. Specified in milliseconds.  Requires the `openOnHover` prop. |
| `handle` | `PopoverHandle<unknown>` | no | A handle to associate the trigger with a popover. |
| `id` | `string` | no | ID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trigger for the popover in controlled mode (with the PopoverRoot `triggerId` prop). |
| `nativeButton` | `boolean` | no | Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`). Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (e.g. `<div>`). |
| `openOnHover` | `boolean` | no | Whether the popover should also open when the trigger is hovered. |
| `payload` | `unknown` | no | A payload to pass to the popover when it is opened. |
| `render` | `ComponentRenderFn<HTMLProps, PopoverTriggerState> \| ReactElement<unknown, string \| JSXElementConstructor<any>>` | no | Allows you to replace the component's HTML element with a different tag, or compose it with another component.  Accepts a `ReactElement` or a function that returns the element to render. |

> [!NOTE]
> **Based on shadcn/ui**
>
> This page adapts the Popover documentation of shadcn/ui (MIT licence) to this
> package; see [Third-party notices](/third-party-notices).
