Skip to main content

Popover

Floating content anchored to a trigger element with click-outside dismissal.

import * as Popover from '@gentleduck/primitives/popover'
import * as Popover from '@gentleduck/primitives/popover'

Anatomy

<Popover.Root>
  <Popover.Trigger />
  <Popover.Anchor />      {/* Optional custom anchor point */}
  <Popover.Portal>
    <Popover.Content>
      <Popover.Arrow />    {/* Optional */}
      <Popover.Close />    {/* Optional */}
    </Popover.Content>
  </Popover.Portal>
</Popover.Root>
<Popover.Root>
  <Popover.Trigger />
  <Popover.Anchor />      {/* Optional custom anchor point */}
  <Popover.Portal>
    <Popover.Content>
      <Popover.Arrow />    {/* Optional */}
      <Popover.Close />    {/* Optional */}
    </Popover.Content>
  </Popover.Portal>
</Popover.Root>

Example

import * as Popover from '@gentleduck/primitives/popover'
 
function UserMenu() {
  return (
    <Popover.Root>
      <Popover.Trigger className="px-3 py-1 border rounded">
        Settings
      </Popover.Trigger>
 
      <Popover.Portal>
        <Popover.Content
          className="bg-white shadow-lg rounded-lg p-4 w-64 border"
          sideOffset={5}
        >
          <p className="font-medium mb-2">User settings</p>
          <input placeholder="Display name" className="w-full border rounded px-2 py-1 mb-2" />
          <Popover.Close className="text-sm text-blue-600">Done</Popover.Close>
          <Popover.Arrow className="fill-white" />
        </Popover.Content>
      </Popover.Portal>
    </Popover.Root>
  )
}
import * as Popover from '@gentleduck/primitives/popover'
 
function UserMenu() {
  return (
    <Popover.Root>
      <Popover.Trigger className="px-3 py-1 border rounded">
        Settings
      </Popover.Trigger>
 
      <Popover.Portal>
        <Popover.Content
          className="bg-white shadow-lg rounded-lg p-4 w-64 border"
          sideOffset={5}
        >
          <p className="font-medium mb-2">User settings</p>
          <input placeholder="Display name" className="w-full border rounded px-2 py-1 mb-2" />
          <Popover.Close className="text-sm text-blue-600">Done</Popover.Close>
          <Popover.Arrow className="fill-white" />
        </Popover.Content>
      </Popover.Portal>
    </Popover.Root>
  )
}

API

Popover.Root

PropTypeDefaultDescription
openboolean-Controlled open state
defaultOpenbooleanfalseInitial open state
onOpenChange(open: boolean) => void-Called on state change
modalbooleanfalseEnable modal behavior (focus trap, scroll lock)
dir'ltr' | 'rtl'-Reading direction for keyboard navigation

Popover.Trigger

Toggles the popover. Sets aria-expanded and aria-controls automatically.

Popover.Anchor

Popover.Portal

Portals content to document.body.

Popover.Content

The floating content. Positioned by the Popper engine relative to the trigger.

PropTypeDefaultDescription
side'top' | 'right' | 'bottom' | 'left''bottom'Preferred side
sideOffsetnumber0Distance from anchor in pixels
align'start' | 'center' | 'end''center'Alignment along the side
alignOffsetnumber0Alignment offset in pixels
forceMounttrue-Keep mounted always
onOpenAutoFocus(event) => void-Intercept auto-focus
onCloseAutoFocus(event) => void-Intercept focus restoration
onPointerDownOutside(event) => void-Called on outside click
onFocusOutside(event) => void-Called when focus moves outside
onInteractOutside(event) => void-Called for any outside interaction
onEscapeKeyDown(event) => void-Called on Escape press
trapFocusbooleancontext.openOverride whether focus is trapped inside the content
disableOutsidePointerEventsbooleancontext.openOverride whether pointer events outside are blocked
lockScrollbooleancontext.openOverride whether body scroll is locked while open

Popover.Arrow

Optional visual arrow pointing toward the anchor.

Popover.Close

Button that closes the popover.


Keyboard interactions

KeyAction
Space / EnterToggle popover (on Trigger)
EscapeClose popover
TabMove focus within content, then out (non-modal) or wrap (modal)