Headless / Overlay

Popover

Popover is a non-modal floating panel pinned to a trigger. Unlike Dialog it takes no focus and draws no scrim — the page keeps working while the panel is open.

Loading playground…
Updated 3 days agoEdit on GitHubWeb & native

What it is

A compound component for anchored, non-modal content. Popover.Content renders into a Portal and positions itself against the trigger using the placement prop. It closes on Escape and on a click outside — there is no scrim, and focus is never trapped.

Popover sits between Dialog and Tooltip. Use it for content that is interactive but not demanding: a filter panel, a settings menu, a small form. Modal interruptions belong in Dialog; purely descriptive text belongs in Tooltip.

Install

example.tsx
yarn add @usemotif/headless
example.tsx
import { Popover } from '@usemotif/headless';

Anatomy

example.tsx
<Popover.Root>
<Popover.Trigger>
  <Button>Filter</Button>
</Popover.Trigger>
<Popover.Content placement="bottom">
  <FilterControls />
  <Popover.Close><Button>Apply</Button></Popover.Close>
</Popover.Content>
</Popover.Root>

API

Popover.Root

Popover.Root

stable
function Popover.Root(props: PopoverRootProps): JSX.Element
openboolean

Controlled open state. Pass alongside `onOpenChange`.

defaultOpenboolean= false

Initial open state for the uncontrolled mode.

onOpenChange(open: boolean) => void

Called whenever the open state changes.

childrenReactNode

The Trigger and Content parts.

Popover.Trigger

Popover.Trigger

stable
function Popover.Trigger(props: PopoverTriggerProps): JSX.Element
childrenReactElementrequired

A single element. Popover clones it to add the toggle handler, `aria-expanded`, and `aria-controls`.

Popover.Content

Popover.Content

stable
function Popover.Content(props: PopoverContentProps): JSX.Element | null
placementPlacement= "bottom"

Where the panel sits relative to the trigger — `top`, `bottom`, `left`, `right`, and the corner variants.

offsetnumber= 8

Pixel gap between the trigger and the panel.

dismissOnClickOutsideboolean= true

Close when the user clicks outside the panel.

dismissOnEscapeboolean= true

Close when the user presses Escape. Focus returns to the trigger.

styleCSSProperties

Inline style for the panel — positioning is applied on top.

childrenReactNode

The panel content.

Popover.Close

Popover.Close

stable
function Popover.Close(props: PopoverCloseProps): JSX.Element
childrenReactElementrequired

A single element. Popover clones it to add a click handler that closes the panel.

Accessibility

The trigger carries aria-expanded and aria-controls, so assistive technology announces that it opens a panel and which one. The panel itself has role="dialog".

Because Popover does not trap focus, the panel's content must be reachable on its own — every interactive element inside needs to be keyboard-focusable in DOM order. Escape closes the panel and returns focus to the trigger, so a keyboard user is never stranded.

Examples

A filter panel:

example.tsx
<Popover.Root>
<Popover.Trigger><Button>Filters</Button></Popover.Trigger>
<Popover.Content placement="bottom-start" style={{ padding: 16 }}>
  <VStack gap="$2">
    <Checkbox.Root><Checkbox.Label>Unread</Checkbox.Label></Checkbox.Root>
    <Checkbox.Root><Checkbox.Label>Starred</Checkbox.Label></Checkbox.Root>
  </VStack>
</Popover.Content>
</Popover.Root>