# Popover (/docs/popover)





`Popover` is the kit's floating **panel**, a styled layer over [Ark UI's Popover](https://ark-ui.com/). Ark owns the open/close state, positioning, focus and `Esc` / click-outside; the kit adds the portaled, animated panel chrome and three composable slots. It's a *panel, not a menu* — you render your own rows inside `Body`, so it hosts anything: a custom picker, a filter popover, the [`TreeMultiSelect`](/docs/tree-multi-select) tree.

```tsx
import { Popover } from '@42-central/ui-react/popover';

<Popover.Root open={open} onOpenChange={(d) => setOpen(d.open)}>
  <Popover.Trigger asChild>
    <Button>Open</Button>
  </Popover.Trigger>
  <Popover.Content>
    <Popover.Header>…</Popover.Header>   {/* fixed */}
    <Popover.Body>…</Popover.Body>       {/* scrolls */}
    <Popover.Footer>…</Popover.Footer>   {/* fixed */}
  </Popover.Content>
</Popover.Root>
```

## Basic usage [#basic-usage]

`open` / `onOpenChange` make it controlled. `Header` and `Footer` stay fixed while `Body` scrolls; the panel is portaled, so it escapes `overflow: hidden` ancestors, and its height tracks the available space up to a cap.

<Preview align="center" code="&#x22;const [open, setOpen] = useState(false);\n\n<Popover.Root open={open} onOpenChange={(d) => setOpen(d.open)}>\n  <Popover.Trigger asChild>\n    <Button>Open popover</Button>\n  </Popover.Trigger>\n  <Popover.Content>\n    <Popover.Header>Filters</Popover.Header>\n    <Popover.Body className=\&#x22;flex flex-col gap-3\&#x22;>\n      <Text size=\&#x22;sm\&#x22; c=\&#x22;muted\&#x22;>Narrow the list down to what you're looking for.</Text>\n      <Switch label=\&#x22;Show archived\&#x22; size=\&#x22;sm\&#x22; />\n      <Switch label=\&#x22;Only mine\&#x22; size=\&#x22;sm\&#x22; defaultChecked />\n    </Popover.Body>\n    <Popover.Footer>\n      <Button size=\&#x22;xs\&#x22; variant=\&#x22;subtle\&#x22;>Reset</Button>\n      <Button size=\&#x22;xs\&#x22;>Apply</Button>\n    </Popover.Footer>\n  </Popover.Content>\n</Popover.Root>&#x22;">
  <PopoverExample />
</Preview>

## Positioning [#positioning]

`width="target"` (default) matches the trigger's width; `position` sets the placement and `offset` the gap. The panel flips and shifts to stay in view (Ark / Floating UI).

<Preview align="center" code="&#x22;<Popover.Root position=\&#x22;bottom-start\&#x22; width=\&#x22;target\&#x22;>…</Popover.Root>\n<Popover.Root position=\&#x22;top\&#x22; width=\&#x22;auto\&#x22; offset={8}>…</Popover.Root>&#x22;">
  <div className="flex flex-wrap items-center gap-2">
    <PopoverExample label="bottom-start target width" position="bottom-start" />

    <PopoverExample label="top auto width" width="auto" position="top" />

    <PopoverExample label="auto width" width="auto" />
  </div>
</Preview>

## Hiding when detached [#hiding-when-detached]

By default, the panel hides itself once its trigger is fully clipped by its
own scrolling ancestor — no separate `open` management needed, it stays hidden
(not closed) and reappears the moment the trigger scrolls back into view.
Useful for a trigger that lives inside a scrollable list or grid, where a
floating panel that keeps following the trigger's (now off-screen) coordinates
would otherwise drift or overlap unrelated content. Pass
`hideWhenDetached={false}` to opt out and keep the old "follow the trigger's
last known position" behavior.

<Preview align="center" code="`<Popover.Root>…</Popover.Root> // hides on scroll-out by default`">
  <PopoverHideWhenDetachedExample />
</Preview>

## Hover mode [#hover-mode]

`mode="hover"` adds a delayed hover preview on top of the click — the "hover a login, see a profile card" pattern, with content people can act on. Hovering the trigger opens a light **preview**; a click, a tap or Enter **pins** it as a regular popover. A click on an open preview pins it rather than closing it, and so does pressing or focusing anything inside it.

<Preview align="center" code="&#x22;<Popover.Root mode=\&#x22;hover\&#x22; width=\&#x22;auto\&#x22;>\n  <Popover.Trigger asChild>\n    <button type=\&#x22;button\&#x22;>\n      <Avatar name=\&#x22;Farid Karim\&#x22; color=\&#x22;name\&#x22; size=\&#x22;sm\&#x22; />\n      fkarim\n    </button>\n  </Popover.Trigger>\n  <Popover.Content className=\&#x22;flex w-72 flex-col gap-3 p-3\&#x22;>\n    <div className=\&#x22;flex items-center gap-3\&#x22;>\n      <Avatar name=\&#x22;Farid Karim\&#x22; color=\&#x22;name\&#x22; />\n      <div className=\&#x22;flex min-w-0 flex-1 flex-col\&#x22;>\n        <Text size=\&#x22;sm\&#x22; span className=\&#x22;font-medium\&#x22;>Farid Karim</Text>\n        <Text size=\&#x22;xs\&#x22; c=\&#x22;muted\&#x22; span>fkarim · Paris · e2r4p11</Text>\n      </div>\n      <Copy value=\&#x22;fkarim\&#x22; variant=\&#x22;subtle\&#x22; color=\&#x22;neutral\&#x22; size=\&#x22;sm\&#x22; />\n    </div>\n    <Button asChild size=\&#x22;xs\&#x22; variant=\&#x22;light\&#x22; color=\&#x22;neutral\&#x22;>\n      <a href=\&#x22;/users/fkarim\&#x22;>View profile</a>\n    </Button>\n  </Popover.Content>\n</Popover.Root>&#x22;">
  <PopoverHoverExample />
</Preview>

| Input     | Preview                                                                                                                                                                  | Pinned                                                                             |
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------- |
| Mouse     | Hover the trigger for `openDelay` (600ms) to open; leave both the trigger and the panel for `closeDelay` (300ms) to close. Crossing from one to the other keeps it open. | Click the trigger, or press inside the preview. Pointer-leave no longer closes it. |
| Touch     | — (touch has no hover)                                                                                                                                                   | Tap the trigger.                                                                   |
| Keyboard  | —                                                                                                                                                                        | Enter or Space on the trigger, or Tab into an open preview.                        |
| Focus     | Never moved, never restored.                                                                                                                                             | Moves into the panel, and back to the trigger on close.                            |
| Closes on | Leaving, Escape, outside click.                                                                                                                                          | A second click on the trigger, Escape, outside click, Tab out.                     |

A preview is never `modal`; a pinned panel takes your `modal` / `autoFocus` / `restoreFocus` as usual. Make the trigger a **button**, not a link: the click belongs to the popover, so put the link inside the card. For a pure, non-interactive link preview — where the trigger *is* the link — [`HoverCard`](/docs/hover-card) is the right tool. `openDelay` and `closeDelay` only exist with `mode="hover"`; on a click popover they're a type error.

## Anatomy [#anatomy]

* **`Popover.Root`** — wraps Ark's `Popover.Root`; owns open state + positioning.
* **`Popover.Trigger`** — the trigger; use `asChild` to wrap your own element.
* **`Popover.Content`** — the portaled, animated panel.
* **`Popover.Header` / `Popover.Footer`** — fixed, non-scrolling slots.
* **`Popover.Body`** — the scroll region; render your rows (and any empty message) here.

### `testId` [#testid]

`Popover.Root` is a headless provider with no DOM node of its own, so it takes no `testId`. `Popover.Content` — the real floating panel — takes its own `testId`, as do `Popover.Header`, `Popover.Footer`, and `Popover.Body`, each landing on its own rendered element. `Popover.Trigger` wraps Ark's trigger and passes every prop through, so an arbitrary `data-testid` lands on it natively. See [Testing](/docs/testing).

## API [#api]

`Popover.Root` props (beyond Ark's `Popover.Root`):

<TypeTable
  type="{
  mode: { type: '&#x22;click&#x22; | &#x22;hover&#x22;', default: '&#x22;click&#x22;', description: 'Open on click only, or also on a delayed hover preview that a click pins. See Hover mode.' },
  openDelay: { type: 'number', default: '600', description: 'mode=&#x22;hover&#x22; only — hover time before the preview opens.' },
  closeDelay: { type: 'number', default: '300', description: 'mode=&#x22;hover&#x22; only — time after the pointer leaves the trigger and the panel before the preview closes.' },
  width: { type: '&#x22;target&#x22; | &#x22;auto&#x22;', default: '&#x22;target&#x22;', description: 'Match the trigger width, or size to content.' },
  position: { type: 'Placement', description: 'Panel placement, e.g. &#x22;bottom-start&#x22;, &#x22;top&#x22;. Ark default &#x22;bottom&#x22;.' },
  offset: { type: 'number', description: 'Gap in px between trigger and panel.' },
  hideWhenDetached: { type: 'boolean', default: 'true', description: 'Hide the panel once its trigger is fully clipped by a scrolling ancestor, and restore it once back in view.' },
  withinPortal: { type: 'boolean', default: 'true', description: 'Render the panel in a Portal so it escapes clipping ancestors.' },
  container: { type: 'HTMLElement | null', description: 'Portal target. Defaults to document.body.' },
  open: { type: 'boolean', description: 'Controlled open state.' },
  defaultOpen: { type: 'boolean', description: 'Uncontrolled initial open state.' },
  onOpenChange: { type: '(details: { open: boolean }) => void', description: 'Fires when the panel opens or closes.' },
  onClose: { type: '() => void', description: 'Fires on close (any source) — Escape, outside interaction, or a controlled onOpenChange.' },
  autoFocus: { type: 'boolean', default: 'true', description: 'Focus the panel on open (set false to manage focus yourself).' },
  disabled: { type: 'boolean', description: 'Prevent opening.' },
}"
/>
