# Popover

A small panel that opens next to its trigger and holds a form or a picker.

```tsx
import {
  Button,
  CopyField,
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from 'ferry-ui'
import { Share2 } from 'lucide-react'

export default function PopoverHero() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button icon={<Share2 />}>Share</Button>
      </PopoverTrigger>
      <PopoverContent aria-label="Share this report" className="flex flex-col gap-3">
        <PopoverHeader>
          <PopoverTitle>Share this report</PopoverTitle>
          <PopoverDescription>Each person with the link can view it.</PopoverDescription>
        </PopoverHeader>
        <CopyField value="https://example.com/s/8f2k" what="share link" size="sm" aria-label="Share link" />
      </PopoverContent>
    </Popover>
  )
}
```

## Usage guidelines

- **A small form or a picker.** Use a popover for a quick edit, a filter or the share settings.
- **A list of actions is a menu.** Use [Dropdown Menu](/docs/components/dropdown-menu).
- **A hint is a tooltip.** For a short text on hover or focus, use [Tooltip](/docs/components/tooltip).
- **Large content needs more room.** Use [Dialog](/docs/components/dialog) or [Sheet](/docs/components/sheet).

## Anatomy

Import the parts. Put them together in this order.

```tsx title="Anatomy"

  Popover,
  PopoverAnchor,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from 'ferry-ui'

<Popover>
  <PopoverAnchor />
  <PopoverTrigger />
  <PopoverContent>
    <PopoverHeader>
      <PopoverTitle />
      <PopoverDescription />
    </PopoverHeader>
  </PopoverContent>
</Popover>
```

| Part | Role |
| --- | --- |
| `Popover` | Holds the open state. |
| `PopoverTrigger` | Opens and closes the popover on a click. |
| `PopoverContent` | The panel. |
| `PopoverHeader` | Holds the title and the description. |
| `PopoverTitle` | The visible title. |
| `PopoverDescription` | A short text below the title. |
| `PopoverAnchor` | An element that sets the position of the panel, in place of the trigger. |

With `asChild`, `PopoverTrigger` gives its behavior to its child element. Use it with a [Button](/docs/components/button).

## Examples

### Side

`PopoverContent` opens below the trigger by default. The `side` prop sets a different side. If the side has no room, the panel changes side.

The `sideOffset` prop sets the gap between the trigger and the panel. The default is 6px.

```tsx
import { Button, Popover, PopoverContent, PopoverTrigger } from 'ferry-ui'

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

export default function PopoverSide() {
  return (
    <>
      {SIDES.map((side) => (
        <Popover key={side}>
          <PopoverTrigger asChild>
            <Button>{side}</Button>
          </PopoverTrigger>
          <PopoverContent side={side} aria-label={`Side ${side}`} className="w-48 text-[13px] text-foreground-light">
            The panel opens on the {side} side of the trigger.
          </PopoverContent>
        </Popover>
      ))}
    </>
  )
}
```

### Alignment

The `align` prop aligns the panel to the start, the center or the end of the trigger. The default is `center`.

```tsx
import { Button, Popover, PopoverContent, PopoverTrigger } from 'ferry-ui'

const ALIGNMENTS = ['start', 'center', 'end'] as const

export default function PopoverAlign() {
  return (
    <>
      {ALIGNMENTS.map((align) => (
        <Popover key={align}>
          <PopoverTrigger asChild>
            <Button>{align}</Button>
          </PopoverTrigger>
          <PopoverContent align={align} aria-label={`Alignment ${align}`} className="w-56 text-[13px] text-foreground-light">
            The panel aligns to the {align} of the trigger.
          </PopoverContent>
        </Popover>
      ))}
    </>
  )
}
```

### Open state

A popover holds its open state by default. To control the state, pass `open` and `onOpenChange`. Use this to close the popover after a submit.

```tsx
import * as React from 'react'
import { Button, Field, Input, Popover, PopoverContent, PopoverTrigger } from 'ferry-ui'

export default function PopoverControlled() {
  const [open, setOpen] = React.useState(false)
  const [name, setName] = React.useState('Billing portal')
  const [draft, setDraft] = React.useState(name)

  function save(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault()
    setName(draft.trim() || name)
    // The form is done: close the popover from the code.
    setOpen(false)
  }

  return (
    <>
      <span className="text-sm font-medium text-foreground">{name}</span>
      <Popover
        open={open}
        onOpenChange={(next) => {
          // Start each edit from the current name.
          if (next) setDraft(name)
          setOpen(next)
        }}
      >
        <PopoverTrigger asChild>
          <Button variant="ghost" size="tiny">
            Rename
          </Button>
        </PopoverTrigger>
        <PopoverContent align="start" aria-label="Rename project">
          <form className="flex flex-col gap-3" onSubmit={save}>
            <Field label="Project name" size="sm" hint="The URL of the project does not change.">
              <Input size="sm" value={draft} onChange={(event) => setDraft(event.target.value)} />
            </Field>
            <div className="flex justify-end gap-2">
              <Button onClick={() => setOpen(false)}>Cancel</Button>
              <Button type="submit" variant="primary">
                Save
              </Button>
            </div>
          </form>
        </PopoverContent>
      </Popover>
    </>
  )
}
```

### Width and padding

The panel is 288px wide and has 16px of padding. Pass classes such as `w-80 p-0` to `PopoverContent` to change them.

```tsx
import { Button, Popover, PopoverContent, PopoverTrigger } from 'ferry-ui'
import { Bell } from 'lucide-react'

const NOTIFICATIONS = [
  { id: 1, text: 'Acme paid the invoice INV-2041.', when: '2 min ago' },
  { id: 2, text: 'Sam Lee joined the workspace.', when: '1 hour ago' },
  { id: 3, text: 'The API key “Analytics export” expires in 3 days.', when: 'Yesterday' },
]

export default function PopoverCustomSize() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="ghost" size="icon" icon={<Bell />} aria-label="Notifications" />
      </PopoverTrigger>
      {/* `w-80 p-0`: a wider panel with no padding, for a list that touches the edges. */}
      <PopoverContent align="end" className="w-80 p-0" aria-label="Notifications">
        <ul className="divide-y">
          {NOTIFICATIONS.map((notification) => (
            <li key={notification.id} className="flex flex-col gap-0.5 px-4 py-2.5">
              <span className="text-[13px] text-foreground">{notification.text}</span>
              <span className="text-xs text-foreground-lighter">{notification.when}</span>
            </li>
          ))}
        </ul>
        <div className="border-t p-2">
          <Button variant="ghost" className="w-full">
            Mark all as read
          </Button>
        </div>
      </PopoverContent>
    </Popover>
  )
}
```

### Anchor

By default the panel opens next to the trigger. Wrap a different element in `PopoverAnchor` to open the panel next to that element.

```tsx
import * as React from 'react'
import { Button, Input, Popover, PopoverAnchor, PopoverContent, PopoverTitle, PopoverTrigger } from 'ferry-ui'
import { CalendarDays } from 'lucide-react'

const DATES = [
  { label: 'End of the month', value: '2026-03-31' },
  { label: 'End of the quarter', value: '2026-06-30' },
  { label: 'End of the year', value: '2026-12-31' },
]

export default function PopoverWithAnchor() {
  const [open, setOpen] = React.useState(false)
  const [date, setDate] = React.useState('2026-03-31')

  return (
    <Popover open={open} onOpenChange={setOpen}>
      {/* The panel aligns to the field and the button together, not to the button only. */}
      <PopoverAnchor asChild>
        <div className="flex w-64 items-center gap-2">
          <Input aria-label="Due date" size="sm" mono value={date} onChange={(event) => setDate(event.target.value)} />
          <PopoverTrigger asChild>
            <Button size="icon" icon={<CalendarDays />} aria-label="Pick a due date" />
          </PopoverTrigger>
        </div>
      </PopoverAnchor>
      <PopoverContent align="start" aria-label="Due date" className="flex w-64 flex-col gap-2">
        <PopoverTitle>Due date</PopoverTitle>
        <div className="flex flex-col gap-1">
          {DATES.map((option) => (
            <Button
              key={option.value}
              variant="ghost"
              className="justify-between"
              onClick={() => {
                setDate(option.value)
                setOpen(false)
              }}
            >
              {option.label}
              <span className="font-mono text-xs text-foreground-lighter">{option.value}</span>
            </Button>
          ))}
        </div>
      </PopoverContent>
    </Popover>
  )
}
```

## Accessibility

- `PopoverTitle` is a plain `<div>`. It is not the accessible name of the panel.
- Give `PopoverContent` an `aria-label` when the panel needs an accessible name.
- The page stays interactive while the popover is open.
- <Kbd>Esc</Kbd> closes the popover. A click outside the panel also closes it.

## API reference

Each part also accepts the props of its Radix UI primitive and the attributes of its element.

### Popover

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` |  |  |
| `open` | `boolean` |  |  |
| `defaultOpen` | `boolean` |  |  |
| `onOpenChange` | `((open: boolean) => void)` |  |  |
| `modal` | `boolean` |  |  |

### PopoverTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### PopoverContent

In ferry-ui, the default `align` is `center` and the default `sideOffset` is 6.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
| `asChild` | `boolean` |  |  |
| `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. |
| `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` |  | Event handler called when the escape key is down. Can be prevented. |
| `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` |  | Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`. 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. |
| `onCloseAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on close. Can be prevented. |
| `side` | `"top" \| "right" \| "bottom" \| "left"` |  |  |
| `sideOffset` | `number` | `6` |  |
| `align` | `"center" \| "start" \| "end"` | `center` |  |
| `alignOffset` | `number` |  |  |
| `arrowPadding` | `number` |  |  |
| `avoidCollisions` | `boolean` |  |  |
| `collisionBoundary` | `Boundary \| Boundary[]` |  |  |
| `collisionPadding` | `number \| Partial<Record<"top" \| "right" \| "bottom" \| "left", number>>` |  |  |
| `sticky` | `"partial" \| "always"` |  |  |
| `hideWhenDetached` | `boolean` |  |  |
| `updatePositionStrategy` | `"always" \| "optimized"` |  |  |

### PopoverHeader

`PopoverHeader` has no props of its own. It accepts the attributes of the element it renders.

### PopoverTitle

`PopoverTitle` has no props of its own. It accepts the attributes of the element it renders.

### PopoverDescription

`PopoverDescription` has no props of its own. It accepts the attributes of the element it renders.

### PopoverAnchor

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `virtualRef` | `RefObject<Measurable \| null>` |  |  |
