nori-ui

Popover

Non-modal floating panel anchored to a trigger — for help content, color pickers, profile previews.

4.0 kBgzipped

At a glance

  • Compose: Popover, PopoverTrigger, PopoverContent. Trigger uses asChild by default — wrap any element (Button, Link, custom Pressable) and it becomes the activator.
  • Cross-platform: web uses position: fixed with a measured trigger rect so it escapes any ancestor overflow: hidden. Native uses RN <Modal> with a transparent backdrop and tap-outside-to-close.
  • Non-modal: focus is NOT trapped — the user can tab back out as normal. Dismisses on outside click and Escape.
  • Accessibility: trigger gets aria-haspopup="dialog" + aria-expanded; content gets role="dialog" (no aria-modal).

Popover vs Tooltip vs Dialog

  • Popover — anchored, non-modal, can hold interactive content (forms, buttons, color pickers). Dismisses on outside click + Escape.
  • Tooltip — anchored, non-interactive, short hover-triggered hints (one or two words to a sentence). Use Tooltip when there's nothing to click inside.
  • Dialog — modal, centered, traps focus, locks scroll. Use Dialog when the user must respond before continuing.

Preview

Direction:

Form inside a popover

The content is non-modal, so a small form (rename, quick edit, filter) fits cleanly without dragging the user into a full dialog.

Direction:

Anatomy

SubcomponentRole
PopoverRoot — owns open state (controlled or uncontrolled).
PopoverTriggerElement that toggles the popover. asChild by default.
PopoverContentThe floating surface. Renders only while open.

Positioning

PopoverContent accepts:

  • side'top' | 'right' | 'bottom' | 'left' (default 'bottom'). Which edge of the trigger to anchor on.
  • align'start' | 'center' | 'end' (default 'center'). Alignment along the chosen edge.

There is a 4-pixel gap between the trigger and the content so the two don't kiss. The minimum width is 200 px so help text never wraps awkwardly.

Open state — open, defaultOpen, onOpenChange

Pass open (paired with onOpenChange) for controlled mode. Pass defaultOpen when you want the popover open on first render (e.g. hint after a route transition). Mixing the two prefers controlled and ignores defaultOpen.

// Uncontrolled — Popover owns its open state.
<Popover>
    <Popover.Trigger><Button>Open</Button></Popover.Trigger>
    <Popover.Content>...</Popover.Content>
</Popover>
 
// Controlled — you own the state, useful for closing from inside the content
// after a save, or for syncing open with route state.
const [open, setOpen] = useState(false);
 
<Popover open={open} onOpenChange={setOpen}>
    <Popover.Trigger><Button>Open</Button></Popover.Trigger>
    <Popover.Content>...</Popover.Content>
</Popover>

Props

Popover

PropTypeDefaultDescription
defaultOpenbooleanfalseUncontrolled initial open state. @defaultValue false
onOpenChange(open: boolean) => voidFires with the new open state.
openbooleanControlled open state.

PopoverTrigger

PropTypeDefaultDescription
asChildbooleantrueRender the child as the trigger (Slot pattern). Default true — pass `false` for an inline pressable.
classNamestring
testIDstring

PopoverContent

PropTypeDefaultDescription
alignenumcenterAlignment along the trigger edge. @defaultValue 'center'
aria-labelstringAccessible label when no visible heading is present.
classNamestring
sideenumbottomSide of the trigger to anchor on. @defaultValue 'bottom'
testIDstring

On this page

Preview theme