Popover

A small panel anchored to the control that opened it — it flips and shifts to stay on screen, and its arrow keeps pointing at the trigger.

Open Source

Installation

Popover ships with the library — install it once, then import:

import { Popover } from "@delacour/react-native-ui/popover";

Or copy the source into your project, to own and edit it:

bunx delacour@latest add popover

Popover draws through the overlay layer, so mount OverlayProvider once at the app root.

Usage

Basic
app/index.tsx
import { Button } from "@/components/ui/button";
import { Popover } from "@/components/ui/popover";
import { View } from "react-native";

export default function Example() {
  return (
    <View className="flex-1 items-center justify-center p-4">
      <Popover>
        <Popover.Trigger asChild>
          <Button variant="secondary">What changed?</Button>
        </Popover.Trigger>
        <Popover.Content>
          <Popover.Arrow />
          <Popover.Close />
          <Popover.Title>Version 2.4</Popover.Title>
          <Popover.Description>Faster sync, offline drafts and a new share sheet.</Popover.Description>
        </Popover.Content>
      </Popover>
    </View>
  );
}

Anatomy

<Popover>
  <Popover.Trigger />
  <Popover.Anchor />
  <Popover.Content>
    <Popover.Arrow />
    <Popover.Close />
    <Popover.Title />
    <Popover.Description />
  </Popover.Content>
</Popover>

Popover.Anchor is optional — without it the panel anchors to the trigger. Write the arrow, the close control, the title and the description only where you want them.

Placement

placement is the side the panel prefers — bottom by default — and align lines it up along that side: start, center or end. It is a preference. When that side lacks room and the opposite side has more, the panel flips; then it slides along the anchor to stay inside the safe area, and the arrow keeps pointing at the trigger's centre.

Placements
<Popover.Content placement="top" align="start" offset={12}>…</Popover.Content>

start and end are logical on top and bottom placements, so they mirror in a right-to-left layout. alignOffset nudges the panel inward from the aligned edge. usePopover().placement tells you which side it actually landed on.

Arrow

Popover.Arrow points from the panel at the anchor's centre — not the panel's — and follows a flip. It stays clear of the panel's rounded corners.

Arrow
<Popover>
  <Popover.Trigger accessibilityLabel="Beta, what does this mean?">
    <Badge variant="soft">Beta</Badge>
  </Popover.Trigger>
  <Popover.Content align="start">
    <Popover.Arrow />
    <Popover.Title>Beta</Popover.Title>
    <Popover.Description>This feature is still changing.</Popover.Description>
  </Popover.Content>
</Popover>

Width and forms

width takes a number, "trigger" for the anchor's width, "full" for the width of the screen inside its safe area, or "content-fit", the default. minWidth raises any of them. The keyboard counts as the bottom of the screen, so a field inside the panel moves it above its trigger rather than typing under the keyboard.

Trigger width, with a field
const [isOpen, setOpen] = useState(false);

<Popover isOpen={isOpen} onOpenChange={setOpen}>
  <Popover.Trigger asChild>
    <Button className="self-stretch" variant="secondary">{name}</Button>
  </Popover.Trigger>
  <Popover.Content align="start" minWidth={260} width="trigger">
    <Popover.Arrow />
    <Popover.Title>Rename</Popover.Title>
    <Input autoFocus onChangeText={setDraft} value={draft} />
    <View className="flex-row justify-end gap-2">
      <Popover.Close asChild>
        <Button size="sm" variant="ghost">Cancel</Button>
      </Popover.Close>
      <Button onPress={save} size="sm">Save</Button>
    </View>
  </Popover.Content>
</Popover>;

Scrolling

maxHeight caps the panel — it is clamped to the room on the side the panel lands on anyway — and isScrollable scrolls what does not fit. The arrow is lifted out of the scrolling body, so it is never scrolled away or clipped.

Scrollable
<Popover.Content isScrollable maxHeight={240} minWidth={220}>
  <Popover.Arrow />
  <ListGroup variant="transparent">
    <ListGroup.Item onPress={() => pick("Auckland")}>
      <ListGroup.ItemContent>
        <ListGroup.ItemTitle>Auckland</ListGroup.ItemTitle>
      </ListGroup.ItemContent>
    </ListGroup.Item>
  </ListGroup>
</Popover.Content>

Dismissing

A tap outside the panel closes it, and so do Android's back button and the iOS escape gesture. The outside tap is swallowed — it never presses what is under it. hasScrim dims the app behind the panel; without it the app stays visible and an outside tap still closes the popover. isDismissible={false} leaves only your own controls able to close it.

Scrim
<Popover isDismissible={false}>…</Popover>
<Popover.Content hasScrim>…</Popover.Content>

Custom surface

isUnstyled drops the panel's fill, border, corner and padding. Draw your own behind the content with background.

<Popover.Content
  background={<View className="flex-1 rounded-3xl bg-primary" />}
  className="px-5 py-4"
  isUnstyled
>
  <Text.Label className="text-primary-foreground">Drawn by the caller.</Text.Label>
</Popover.Content>

Where it draws

The panel is drawn in the overlay layer, above every bottom sheet and above a dialog it was opened from, and over the navigator's header. It is measured in window coordinates, so a trigger inside a ScrollView or a bottom sheet places it correctly. Rotating the device re-places an open popover.

Accessibility

The panel is a modal dialog for assistive technology: focus is held inside it, moves to the title when it opens and back to the trigger when it closes, and the panel is labelled by Popover.Title. The trigger reports whether the panel is expanded. The escape gesture closes it. Under Reduce Motion the panel fades without sliding or scaling.

API Reference

Popover

Prop

Type

Popover.Trigger

Takes Pressable's props. With asChild it hands the toggle to its child's onPress instead of wrapping it, so the child must be built on Pressable — a Button, a ListGroup.Item.

Popover.Anchor

Takes React Native's ViewProps, plus asChild. While it is mounted the panel anchors to it instead of the trigger.

Popover.Content

Takes React Native's ViewProps.

Prop

Type

Popover.Arrow

Prop

Type

Popover.Title, Popover.Description

Take Text's props. The title is a Text.Label announced as a header; the description is a muted Text.Caption.

Popover.Close

Without asChild, a ✕ in the panel's top-right corner, labelled Close. With asChild, it closes the popover from its child's onPress. Takes Pressable's props either way.

usePopover

Reads the enclosing popover. Throws outside a Popover.

Prop

Type

On this page