|Kit

Popover

A floating panel on Ark UI's Popover — portaled, width-matched to the trigger, animated, with composable Header / Body / Footer slots.

Popover is the kit's floating panel, a styled layer over Ark UI's Popover. 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 tree.

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

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.

Code

const [open, setOpen] = useState(false);

<Popover.Root open={open} onOpenChange={(d) => setOpen(d.open)}>
  <Popover.Trigger asChild>
    <Button>Open popover</Button>
  </Popover.Trigger>
  <Popover.Content>
    <Popover.Header>Filters</Popover.Header>
    <Popover.Body className="flex flex-col gap-3">
      <Text size="sm" c="muted">Narrow the list down to what you're looking for.</Text>
      <Switch label="Show archived" size="sm" />
      <Switch label="Only mine" size="sm" defaultChecked />
    </Popover.Body>
    <Popover.Footer>
      <Button size="xs" variant="subtle">Reset</Button>
      <Button size="xs">Apply</Button>
    </Popover.Footer>
  </Popover.Content>
</Popover.Root>

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).

Code

<Popover.Root position="bottom-start" width="target">…</Popover.Root>
<Popover.Root position="top" width="auto" offset={8}>…</Popover.Root>

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.

Code

<Popover.Root>…</Popover.Root> // hides on scroll-out by default

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.

Code

<Popover.Root mode="hover" width="auto">
  <Popover.Trigger asChild>
    <button type="button">
      <Avatar name="Farid Karim" color="name" size="sm" />
      fkarim
    </button>
  </Popover.Trigger>
  <Popover.Content className="flex w-72 flex-col gap-3 p-3">
    <div className="flex items-center gap-3">
      <Avatar name="Farid Karim" color="name" />
      <div className="flex min-w-0 flex-1 flex-col">
        <Text size="sm" span className="font-medium">Farid Karim</Text>
        <Text size="xs" c="muted" span>fkarim · Paris · e2r4p11</Text>
      </div>
      <Copy value="fkarim" variant="subtle" color="neutral" size="sm" />
    </div>
    <Button asChild size="xs" variant="light" color="neutral">
      <a href="/users/fkarim">View profile</a>
    </Button>
  </Popover.Content>
</Popover.Root>
InputPreviewPinned
MouseHover 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.
FocusNever moved, never restored.Moves into the panel, and back to the trigger on close.
Closes onLeaving, 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 is the right tool. openDelay and closeDelay only exist with mode="hover"; on a click popover they're a type error.

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

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.

API

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

Prop

Type

Changelog

v0.18.0
  • asChild is a compile error on Combobox.Control, Group, Content, Panel and Item, Drawer/Modal.Positioner, Drawer.Grabber, Popover.Content and Tooltip.Arrow

    #294
v0.16.0
  • mode="hover": a hover preview that a click pins open

    #255
  • An outside click no longer dismisses the parent while a nested overlay is open

    #251
v0.14.0
  • Popovers open beside the chip inside the view's own scroller and mount lazily; Popover accepts Ark's full positioning options

    #209
v0.9.0
  • Opened inside a Modal or Drawer, an overlay portals into it, so it always paints above its host

    #135
v0.6.0
  • hideWhenDetached, on by default, hides the panel once its trigger scrolls out of view

    #85
v0.4.0
  • Floating surfaces take a brand tint

    #41
v0.3.1
  • Borders and dividers on floating panels match the panel's own surface

    #37
v0.3.0
  • onClose fires alongside onOpenChange, as on Modal

    #34
  • Panels and highlights use translucent washes instead of opaque grays

    #35
v0.0.1
  • Popover: a click-triggered panel

On this page