BetaOverlays

Popover

A panel anchored to whatever opened it. The component the Select was written before: the two share their positioning, their measuring pass and their entrance.

Overview

A panel anchored to whatever opened it. The component the Select was written before: the two share their positioning, their measuring pass and their entrance.

First example

<Popover>
<Popover.Trigger asChild>
<Button variant="tertiary">Détails</Button>
</Popover.Trigger>
<Popover.Overlay />
<Popover.Content placement="top">
<Popover.Title>Livraison</Popover.Title>
<Popover.Description>Sous trois jours ouvrés.</Popover.Description>
</Popover.Content>
</Popover>

Anatomy

<Popover>
  <Popover.Trigger>…</Popover.Trigger>
  <Popover.Overlay />
  <Popover.Content>
    <Popover.Title>…</Popover.Title>
    <Popover.Description>…</Popover.Description>
    <Popover.Close>…</Popover.Close>
  </Popover.Content>
</Popover>
  • Popover — state and resolved style. It renders no node.
  • Popover.Trigger — what opens the panel, and the rectangle it anchors to.
  • Popover.Overlay — the backdrop. Optional, and what closes the panel on a press outside.
  • Popover.Content — the panel, positioned against the trigger, in a portal.
  • Popover.Title / Popover.Description — its two texts.
  • Popover.Close — anything that closes it.

Popover.Overlay and Popover.Content render into the nearest PortalHost rather than where they are written. Their place in the JSX says when they exist, not where they appear.

Usage

Basic

<Popover>
  <Popover.Trigger asChild>
    <Button variant="tertiary">Détails</Button>
  </Popover.Trigger>
  <Popover.Overlay />
  <Popover.Content placement="top">
    <Popover.Title>Livraison</Popover.Title>
    <Popover.Description>Sous trois jours ouvrés.</Popover.Description>
  </Popover.Content>
</Popover>

asChild is the normal way to write the trigger. It paints nothing of its own — a popover's trigger is usually a Button, an Icon or a word in a sentence, and giving it a surface would put a second box around one of those.

Placement

<Popover.Content placement="end" align="start" offset={12} width={280} />

placement takes all four sides, which is what separates this from Select.Content. A select's list is as wide as the field it drops out of, and one hanging off the side of that field reads as a menu; a popover belongs to nothing, so it can sit anywhere.

align runs along the axis the side does not pin — horizontal for a panel above or below, vertical for one beside — and clamps to the screen insets.

avoidCollisions is on by default and flips to the opposite side, but only when that side has strictly more room. A panel two points short stays where it was asked, because a flip on a near miss reads as a glitch.

Both axes are clamped to the insets, whether or not it flips. The side decides where the panel wants to go; the insets decide where it is allowed to be. A panel beside a trigger with no room for it is pushed inside the screen and may overlap the trigger — which is the right trade: a panel covering the button that opened it is legible, and a panel past the edge of the screen is not.

No backdrop

Omit Popover.Overlay and nothing captures the press outside: a Popover.Close, or your own state, is what closes the panel. That is the answer for a popover inside a sheet that already dims its background.

Controlled

const [isOpen, setOpen] = useState(false)

<Popover isOpen={isOpen} onOpenChange={setOpen}>…</Popover>

Style as props

<Popover.Content padding={20} backgroundColor="#111" />
<Popover.Title fontSize={22} />

Full RN names, full RN values (R14). Every node takes them.

Root props

PopoverProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
radiusRadiusKey | undefinedNo
isOpenboolean | undefinedNoControlled. Leave unset and the root owns it.
defaultOpenboolean | undefinedNo
onOpenChange((isOpen: boolean) => void) | undefinedNo
isDisabledboolean | undefinedNo

Slots

PopoverCloseProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
asChildboolean | undefinedNo

PopoverContentProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
placementPlacement | undefinedNo
alignAlign | undefinedNo
widthAnchoredWidth | undefinedNo
offsetnumber | undefinedNoDistance from the trigger, in points.
alignOffsetnumber | undefinedNoShift along the alignment axis, in points.
avoidCollisionsboolean | undefinedNoFlip to the opposite side when the chosen one does not fit.
insetsInsets | undefinedNo

PopoverDescriptionProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

PopoverOverlayProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
isDismissableboolean | undefinedNoPressing the backdrop closes the panel. Set false and only a `Close` closes it.

PopoverTitleProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo

PopoverTriggerProps

The node's inherited React Native props apply too, and so do its style props padding, margin, width and the rest.

PropTypeRequiredDescription
childrenReactNodeNo
asChildboolean | undefinedNo

Variants, sizes and colour

This component adds no visual axis of its own. The values it does take are in the generated types above and in the live demo.

Accessibility

The trigger is a button carrying expanded. The overlay announces nothing at all — it is the absence of the panel, and "button" spoken over the whole screen is worse than silence.

Migration from legacy

This component declares no rule of its own. The migration guide covers variants, colours and slots.

Implementation notes

Motion

The panel grows out of the trigger: 200 ms in from scale: 0.95, offset eight points towards it, so a panel below enters upwards, one above enters downwards, one beside enters sideways. Exit mirrors it at 150 ms — closing is an acknowledgement rather than an arrival, and a dismissal as long as the opening feels like the control is arguing.

The measuring pass. The panel mounts invisibly for one frame to learn how big it wants to be, then places itself and plays its entrance. Without it avoidCollisions has nothing to compare, and a panel that does not fit on the side it was asked for would open off the screen. That frame is why the entrance is keyed on the resolved placement rather than started at mount.

What it shares with the `Select`

sharedwhere
the placement arithmeticutils/placement.ts
the trigger's measurementhooks/use-anchor-ref.ts
the measuring pass and the originhooks/use-anchored-position.ts
the entrance and exit keyframessystem/anchored/

Written for the Select first, extracted here — §2 bis: at the second use, never by anticipation. Menu, SubMenu and Tooltip read the same four.

Two of them exist because of bugs rather than tidiness. The trigger measures again on every open, because onLayout never fires on scroll and a trigger inside a ScrollView otherwise reports where it used to be. And the position is computed in the host's coordinates rather than the window's, because the trigger reports itself against the window while the panel is laid out inside the PortalHost — anything the two differ by is exactly the distance the panel would be wrong by.

The portal

XAUIProvider mounts the PortalHost these render into, so there is nothing to do. Without a host anywhere, Portal renders nothing: the popover opens onto an empty screen with no error. That silence is why the provider mounts one by default.