# Popover

A small panel anchored to a button that holds a few controls or a short explanation.

```tsx
import { Button } from "@zenoxai/ui/components/button"
import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "@zenoxai/ui/components/popover"

function PopoverDemo() {
  return (
    <Popover>
      <PopoverTrigger render={<Button variant="outline" />}>
        Snooze
      </PopoverTrigger>
      <PopoverContent>
        <PopoverHeader>
          <PopoverTitle>Snooze alert</PopoverTitle>
          <PopoverDescription>
            Hide it from the queue. It comes back if the rule fires again.
          </PopoverDescription>
        </PopoverHeader>
        <div className="grid grid-cols-3 gap-2">
          <Button variant="outline" size="sm">
            1 hour
          </Button>
          <Button variant="outline" size="sm">
            4 hours
          </Button>
          <Button variant="outline" size="sm">
            1 day
          </Button>
        </div>
      </PopoverContent>
    </Popover>
  )
}
```

## Usage

```tsx
import { Button } from "@zenoxai/ui/components/button"
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@zenoxai/ui/components/popover"
```

```tsx
<Popover>
  <PopoverTrigger render={<Button variant="outline" />}>Snooze</PopoverTrigger>
  <PopoverContent>
    <PopoverHeader>
      <PopoverTitle>Snooze alert</PopoverTitle>
      <PopoverDescription>Hide it from the queue.</PopoverDescription>
    </PopoverHeader>
    {/* controls */}
  </PopoverContent>
</Popover>
```

A popover opens on click next to its trigger and closes on Escape or a click outside. It is not
modal: the rest of the page stays usable. Use it for a few related controls that belong to one
button (snooze options, a "save view" field, a quick filter) or for an explanation the user asks
for.

| Need                                                                         | Use                                        |
| ---------------------------------------------------------------------------- | ------------------------------------------ |
| A list of actions or options to pick from                                    | [Dropdown Menu](/components/dropdown-menu) |
| A task that needs the user's full attention, or more than a couple of fields | [Dialog](/components/dialog)               |
| A one-line label for an icon button                                          | [Tooltip](/components/tooltip)             |
| A preview of what a link points to, on hover                                 | [Hover Card](/components/hover-card)       |

`PopoverContent` is 18rem wide, opens below the trigger with its center aligned, and lays out its
children in a column with a small gap. Change the width with `className`, the position with
`side`, `align`, `sideOffset` and `alignOffset`.

## Examples

### With a form

Keep a popover form to one or two fields and a single action. Anything longer belongs in a
[Dialog](/components/dialog) or a [Sheet](/components/sheet).

```tsx
import { Button } from "@zenoxai/ui/components/button"
import { Field, FieldLabel } from "@zenoxai/ui/components/field"
import { Input } from "@zenoxai/ui/components/input"
import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "@zenoxai/ui/components/popover"

function PopoverWithForm() {
  return (
    <Popover>
      <PopoverTrigger render={<Button variant="outline" />}>
        Save view
      </PopoverTrigger>
      <PopoverContent align="start">
        <PopoverHeader>
          <PopoverTitle>Save view</PopoverTitle>
          <PopoverDescription>
            Keeps the current filters and columns under a name.
          </PopoverDescription>
        </PopoverHeader>
        <Field>
          <FieldLabel htmlFor="view-name">Name</FieldLabel>
          <Input id="view-name" placeholder="Critical alerts, Acme Corp" />
        </Field>
        <Button size="sm" className="self-end">
          Save view
        </Button>
      </PopoverContent>
    </Popover>
  )
}
```

### Side

`side` is `bottom` by default. The popover flips to the opposite side and shifts along the edge
when the preferred position would overflow the viewport.

```tsx
import { Button } from "@zenoxai/ui/components/button"
import { Popover, PopoverContent, PopoverDescription, PopoverTrigger } from "@zenoxai/ui/components/popover"

function PopoverSides() {
  return (
    <>
      {(["top", "right", "bottom", "left"] as const).map((side) => (
        <Popover key={side}>
          <PopoverTrigger render={<Button variant="outline" />}>
            {side}
          </PopoverTrigger>
          <PopoverContent side={side} className="w-56">
            <PopoverDescription>
              Opens on the {side}. It flips to the opposite side when there is
              no room.
            </PopoverDescription>
          </PopoverContent>
        </Popover>
      ))}
    </>
  )
}
```

### Open on hover

For an info icon that explains a figure, set `openOnHover` on the trigger. It still opens on
click and keyboard, so touch and screen reader users can reach the text, which a
[Tooltip](/components/tooltip) does not allow. `delay` sets the hover delay in milliseconds.

```tsx
import { InfoIcon } from "lucide-react"
import { Button } from "@zenoxai/ui/components/button"
import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger } from "@zenoxai/ui/components/popover"

function PopoverOpenOnHover() {
  return (
    <div className="flex items-center gap-1.5">
      <span>Risk score: 82</span>
      <Popover>
        <PopoverTrigger
          openOnHover
          delay={200}
          render={
            <Button
              variant="ghost"
              size="icon-xs"
              aria-label="How the risk score is calculated"
            />
          }
        >
          <InfoIcon />
        </PopoverTrigger>
        <PopoverContent side="top">
          <PopoverHeader>
            <PopoverTitle>Risk score</PopoverTitle>
            <PopoverDescription>
              Combines the severity of open alerts, exposed vulnerabilities and
              how critical the asset is. Above 70 is high.
            </PopoverDescription>
          </PopoverHeader>
        </PopoverContent>
      </Popover>
    </div>
  )
}
```

## Accessibility

- The trigger gets `aria-expanded` and `aria-haspopup`. Focus moves into the popover when it
  opens and returns to the trigger when it closes.
- `PopoverTitle` and `PopoverDescription` label and describe the popup. Use them instead of
  plain text when the popover has a heading.
- An icon-only trigger needs an `aria-label` that says what the popover holds.

## API reference

`import { ... } from "@zenoxai/ui/components/popover"`

### Popover

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| actionsRef | `RefObject<PopoverRootActions \| null>` |  | 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. |
| defaultOpen | `boolean` | `false` | Whether the popover is initially open.  To render a controlled popover, use the `open` prop instead. |
| defaultTriggerId | `string \| null` |  | 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>` |  | A handle to associate the popover with a trigger. If specified, allows external triggers to control the popover's open state. |
| modal | `boolean \| "trap-focus"` | `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.  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: PopoverRootChangeEventDetails) => void)` |  | Event handler called when the popover is opened or closed. |
| onOpenChangeComplete | `((open: boolean) => void)` |  | Event handler called after any animations complete when the popover is opened or closed. |
| open | `boolean` |  | Whether the popover is currently open. |
| triggerId | `string \| null` |  | 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). |

### PopoverContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| align | `"center" \| "start" \| "end"` | `start` | How to align the popup relative to the specified side. |
| alignOffset | `number \| OffsetFunction` | `0` | 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. @example ```jsx <Positioner   alignOffset={({ side, align, anchor, positioner }) => {     return side === 'top' \|\| side === 'bottom'       ? anchor.width       : anchor.height;   }} /> ``` |
| finalFocus | `boolean \| RefObject<HTMLElement \| null> \| ((closeType: InteractionType) => boolean \| void \| HTMLElement \| null)` |  | 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) => boolean \| void \| HTMLElement \| null)` |  | 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 | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, PopoverPopupState>` |  | 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 | `"left" \| "right" \| "top" \| "bottom" \| "inline-end" \| "inline-start"` | `bottom` | Which side of the anchor element to align the popup against. May automatically change to avoid collisions. |
| sideOffset | `number \| OffsetFunction` | `0` | 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. @example ```jsx <Positioner   sideOffset={({ side, align, anchor, positioner }) => {     return side === 'top' \|\| side === 'bottom'       ? anchor.height       : anchor.width;   }} /> ``` |

### PopoverDescription

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, PopoverDescriptionState>` |  | 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

No props of its own.

### PopoverTitle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, PopoverTitleState>` |  | 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 | Default | Description |
| --- | --- | --- | --- |
| closeDelay | `number` | `0` | How long to wait before closing the popover that was opened on hover. Specified in milliseconds.  Requires the `openOnHover` prop. |
| delay | `number` | `300` | How long to wait before the popover may be opened on hover. Specified in milliseconds.  Requires the `openOnHover` prop. |
| handle | `PopoverHandle<unknown>` |  | A handle to associate the trigger with a popover. |
| nativeButton | `boolean` | `true` | 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>`). |
| openOnHover | `boolean` | `false` | Whether the popover should also open when the trigger is hovered. |
| payload | `unknown` |  | A payload to pass to the popover when it is opened. |
| render | `ReactElement<unknown, string \| JSXElementConstructor<any>> \| ComponentRenderFn<HTMLProps, PopoverTriggerState>` |  | 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. |

