# Popover

A small panel of extra content or controls next to the element that opened it. The page stays usable.

- **Import:** `import { Popover, PopoverTrigger, PopoverContent, PopoverAnchor, PopoverHeader, PopoverTitle, PopoverDescription } from "@booleanpress/ui/popover"`
- **Radix Popover:** <https://www.radix-ui.com/primitives/docs/components/popover>
- **APG Dialog (Non-Modal):** <https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/>
- **Page:** <https://ui.booleanpress.com/components/popover> · @booleanpress/ui 0.1.0

## Usage

A popover opens when its trigger is pressed and closes on Escape or a click outside. Use it for a short form, a filter, or an explanation. Use a [dialog](/components/dialog) when the page must wait, and a [tooltip](/components/tooltip) for a label that needs no interaction.

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

export function DeliveryStatus() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline">Delivery status</Button>
      </PopoverTrigger>
      <PopoverContent>
        <PopoverHeader>
          <PopoverTitle>Delivered</PopoverTitle>
          <PopoverDescription>The mail server accepted this email.</PopoverDescription>
        </PopoverHeader>
      </PopoverContent>
    </Popover>
  )
}
```

`PopoverContent` is 288 px wide; set `className="w-80"` for another width. `side` and `align` choose the placement, and it flips to the other side when there is no room. It is uncontrolled, or controlled with `open` and `onOpenChange`. `PopoverAnchor` positions it against an element other than the trigger. There is no per-instance animation switch: motion is set once, in `theme.css`.

## Examples

### Basic

A heading and a sentence of explanation.

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

export default function PopoverBasic() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline">Delivery status</Button>
      </PopoverTrigger>
      <PopoverContent>
        <PopoverHeader>
          <PopoverTitle>Delivered</PopoverTitle>
          <PopoverDescription>The mail server accepted this email at 09:41. The recipient's server has not reported a bounce.</PopoverDescription>
        </PopoverHeader>
      </PopoverContent>
    </Popover>
  )
}
```

### Placement

`side` on each of the four sides; it flips when the window has no room.

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

const SIDES = ["top", "right", "bottom", "left"] as const

export default function PopoverPlacement() {
  return (
    <div className="flex flex-wrap items-center justify-center gap-2">
      {SIDES.map((side) => (
        <Popover key={side}>
          <PopoverTrigger asChild>
            <Button variant="outline" className="capitalize">
              {side}
            </Button>
          </PopoverTrigger>
          <PopoverContent side={side} align="center" className="w-48">
            <PopoverDescription>Opens on the {side} side, and flips when there is no room.</PopoverDescription>
          </PopoverContent>
        </Popover>
      ))}
    </div>
  )
}
```

### With a form

A short form inside; focus moves to the first field when it opens.

```tsx
import { Button } from "@booleanpress/ui/button"
import { Input } from "@booleanpress/ui/input"
import { Label } from "@booleanpress/ui/label"
import { Popover, PopoverContent, PopoverHeader, PopoverTitle, PopoverTrigger } from "@booleanpress/ui/popover"

export default function PopoverForm() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline">Rename mailer</Button>
      </PopoverTrigger>
      <PopoverContent align="start" className="w-80">
        <form className="flex flex-col gap-4" onSubmit={(event) => event.preventDefault()}>
          <PopoverHeader>
            <PopoverTitle>Rename mailer</PopoverTitle>
          </PopoverHeader>
          <div className="flex flex-col gap-2">
            <Label htmlFor="popover-mailer-name">Name</Label>
            <Input id="popover-mailer-name" defaultValue="Primary mailer" />
          </div>
          <Button type="submit" size="sm" className="self-end">
            Save
          </Button>
        </form>
      </PopoverContent>
    </Popover>
  )
}
```

### Controlled

`open` and `onOpenChange` let the content close it.

```tsx
import { useState } from "react"
import { Button } from "@booleanpress/ui/button"
import { Popover, PopoverContent, PopoverDescription, PopoverTrigger } from "@booleanpress/ui/popover"

export default function PopoverControlled() {
  const [open, setOpen] = useState(false)

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button variant="outline">{open ? "Hide" : "Show"} the sending limit</Button>
      </PopoverTrigger>
      <PopoverContent>
        <PopoverDescription>This connection sends up to 500 emails an hour.</PopoverDescription>
        <Button size="sm" className="mt-3" onClick={() => setOpen(false)}>
          Got it
        </Button>
      </PopoverContent>
    </Popover>
  )
}
```

## Accessibility

**Semantics.** The trigger is a `button` with `aria-haspopup="dialog"`, `aria-expanded` and `aria-controls`. The content is a `div` with `role="dialog"`. It is not modal: the page stays visible to assistive technology, and a click outside closes it.

**Labels.** The trigger's text names the content. For content that is a form or a group of controls, add `aria-label` or `aria-labelledby` to `PopoverContent`. `PopoverTitle` and `PopoverDescription` are plain text, not wired to the dialog.

**Focus.** Focus moves into the content when it opens, Tab wraps within it while it is open, and focus returns to the trigger when it closes.

**Known limits.**

- `PopoverTitle` renders a `div`, not a heading, and does not name the dialog by itself.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Enter + Space | On the trigger, opens or closes the popover. |
| Escape | Closes it and returns focus to the trigger. |
| Tab | Moves through the content, wrapping from the last element to the first; Shift+Tab goes the other way. Escape or a click outside closes it. |

## API

### Popover

Renders Radix Popover.Root and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen` | `boolean` |  | Whether it starts open, when it controls itself. |
| `modal` | `boolean` |  | When `true`, the rest of the page cannot be reached while it is open. `false` by default. |
| `onOpenChange` | `((open: boolean) => void)` |  | Called with `true` or `false` when it opens or closes. |
| `open` | `boolean` |  | Whether it is open, when you control it. Pair it with `onOpenChange`. |

### PopoverTrigger

Renders Radix Popover.Trigger and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

### PopoverContent

Renders Radix Popover.Content and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"center" \| "start" \| "end"` |  | Alignment along that side: `start`, `center` or `end`. `center` by default. |
| `alignOffset` | `number` |  |  |
| `arrowPadding` | `number` |  |  |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |
| `avoidCollisions` | `boolean` |  |  |
| `collisionBoundary` | `Boundary \| Boundary[]` |  |  |
| `collisionPadding` | `number \| Partial<Record<"left" \| "right" \| "top" \| "bottom", number>>` |  |  |
| `deferPointerDownOutside` | `boolean` |  | When `true`, a `'pointerdown'` event outside of the layered element will wait for the interaction's click event before dispatching, allowing third-party code to stop propagation of later events and cancel dismissal. |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
| `hideWhenDetached` | `boolean` |  |  |
| `onCloseAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on close. Can be prevented. |
| `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` |  | Event handler called when the escape key is down. Can be prevented. |
| `onFocusOutside` | `((event: FocusOutsideEvent) => void)` |  | Event handler called when the focus moves outside of the `DismissableLayer`. Can be prevented. |
| `onInteractOutside` | `((event: FocusOutsideEvent \| PointerDownOutsideEvent) => void)` |  | Event handler called when an interaction happens outside the `DismissableLayer`. Specifically, when a `pointerdown` event happens outside or focus moves outside of it. Can be prevented. |
| `onOpenAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on open. Can be prevented. |
| `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` |  | Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`. Can be prevented. |
| `side` | `"left" \| "right" \| "top" \| "bottom"` |  | The preferred side: `top`, `right`, `bottom` or `left`. `bottom` by default; it flips when there is no room. |
| `sideOffset` | `number` | `4` | Distance in pixels from the trigger. 4 by default. |
| `sticky` | `"partial" \| "always"` |  |  |
| `updatePositionStrategy` | `"always" \| "optimized"` |  |  |

### PopoverAnchor

Renders Radix Popover.Anchor and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |
| `virtualRef` | `RefObject<Measurable \| null>` |  |  |

### PopoverHeader

Renders a `div` and passes it every other prop.

### PopoverTitle

Renders a `h2` and passes it every other prop.

### PopoverDescription

Renders a `p` and passes it every other prop.

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="popover"` (Popover), `data-slot="popover-trigger"` (PopoverTrigger), `data-slot="popover-content"` (PopoverContent), `data-slot="popover-anchor"` (PopoverAnchor), `data-slot="popover-header"` (PopoverHeader), `data-slot="popover-title"` (PopoverTitle), `data-slot="popover-description"` (PopoverDescription).

## Theming

The panel is a floating surface: `--popover` and `--popover-foreground`, with the border token and a medium shadow.

| Token | Used for |
| --- | --- |
| `--muted-foreground` | text |
| `--popover` | background |
| `--popover-foreground` | text |
