The float surface that menus, listboxes and small panels open in, flipped to fit the screen.

Free content

A popover opens beside its trigger, with the float fill, frame, surface corner and float shadow. Menus and lists of options open in it, and it also holds free content, such as a filter panel whose choices apply as they change. isPadded pads free content 24px. It is off by default, because a list or a menu pads itself.

Example

Follows the page theme and corners
import { Button, Checkbox, CheckboxGroup, DialogTrigger, Popover, Switch } from '@snapshot-ui/react';
import type { ReactElement } from 'react';

export default function PopoverBasic(): ReactElement {
  return (
    <DialogTrigger>
      <Button variant="secondary">Filter the projects</Button>
      <Popover isPadded>
        <div style={{ display: 'grid', gap: 24 }}>
          <CheckboxGroup label="Status" defaultValue={['active']}>
            <Checkbox value="active">Active</Checkbox>
            <Checkbox value="paused">Paused</Checkbox>
            <Checkbox value="archived">Archived</Checkbox>
          </CheckboxGroup>
          <Switch defaultSelected>Only projects I own</Switch>
        </div>
      </Popover>
    </DialogTrigger>
  );
}

Matching the trigger

matchTriggerWidth makes the trigger's width the popover's minimum, so a panel under a wide trigger lines up with both edges. Wider content still widens the popover.

Example

Follows the page theme and corners
import { Button, DialogTrigger, Popover, Radio, RadioGroup } from '@snapshot-ui/react';
import type { ReactElement } from 'react';

export default function PopoverMatchWidth(): ReactElement {
  return (
    <DialogTrigger>
      <Button variant="secondary" style={{ width: '100%', maxWidth: '15rem' }}>
        Sort the projects
      </Button>
      <Popover isPadded matchTriggerWidth>
        <RadioGroup label="Sort by" defaultValue="recent">
          <Radio value="recent">Most recent</Radio>
          <Radio value="name">Name</Radio>
          <Radio value="owner">Owner</Radio>
        </RadioGroup>
      </Popover>
    </DialogTrigger>
  );
}

Usage

Wrap free content and its button in a DialogTrigger. Or control the popover with isOpen and onOpenChange, and point triggerRef at the element it opens from. A menu, a select or a combo box brings its own trigger, so do not wrap it in a DialogTrigger. Popover is a client component that a server component can render, as the examples on this page do.

It opens 8px under its trigger at the start edge. placement and offset change that: placement="top end" opens it above the trigger at the end edge.

  • Fitting: with no room on its side, it flips to the other side, and it shifts along the trigger to stay 12px inside the window.
  • data-placement: names the side the popover landed on.
  • Size: never wider than the screen less 2rem. Taller content scrolls inside it.
  • Right to left: start and end follow the writing direction.
  • No arrow: the gap and the shadow tie it to its trigger.

A popover takes the theme, shape and density of its trigger, so one opened from a dark card is dark on a light page. See Theming.

Props

ref and className go on the popover element, the one that takes focus. className is merged after the library's classes. triggerRef is the element it opens against. A trigger component hands it over. Outside one, a popover opened with isOpen or defaultOpen needs it, or it throws an error that names triggerRef. The rest of React Aria's popover props pass through.

children
((values: PopoverRenderProps & { defaultChildren: ReactNode | undefined; }) => ReactNode) | ReactNode

The content: free content, padded with isPadded, or a list that pads itself.

className
string | ((values: PopoverRenderProps & { defaultClassName: string | undefined; }) => string)

Goes on the popover element, merged after the library's classes.

defaultOpen
boolean

Whether the overlay is open by default (uncontrolled).

isOpen
boolean

Whether the overlay is open by default (controlled).

isPadded
boolean

Pads free content 24px. Lists and menus pad themselves, so it is off by default.

Default: false

matchTriggerWidth
boolean

Makes the trigger's width the popover's minimum.

Default: false

offset
number

The gap to the trigger.

Default: 8

onOpenChange
((isOpen: boolean) => void)

Handler that is called when the overlay's open state changes.

placement
Placement

Default: 'bottom start'

triggerRef
RefObject<Element | null>

The ref for the element which the popover positions itself with respect to. When used within a trigger component such as DialogTrigger, MenuTrigger, Select, etc., this is set automatically. It is only required when used standalone.

Accessibility

Opened from a DialogTrigger, the popover is a dialog named by its trigger. Focus moves into it, and Tab cycles through its controls without reaching the page. Escape or a press outside closes it and returns focus to the trigger. While it is open, the page behind is hidden from assistive technology and does not scroll.

EscapeCloses the popover.
TabCycles through the controls inside.

Design rules

What keeps popovers in the system.

Keep a popover to one small task, such as filtering a list.

Pad free content with isPadded.

Let a list or a menu pad itself.

Never use a popover to ask a question or for a form that saves. That is a dialog.

Never make a popover wider than the screen less 2rem.

Never add an arrow. The gap and the shadow tie it to its trigger.

esc

↑ ↓ to move↵ to open