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

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)