Popover

A non-modal popup anchored to a trigger, positioned with flip/shift and an optional arrow.

Installation

npx nuecrea-ui add popover

Usage

import { Button } from '@/components/ui/button';
import * as Popover from '@/components/ui/popover';

function Example({
  label,
  side,
  align,
  arrowPadding,
  disabled,
}: {
  label: string;
  side?: 'top' | 'bottom' | 'left' | 'right';
  align?: 'start' | 'center' | 'end';
  arrowPadding?: number;
  disabled?: boolean;
}) {
  return (
    <Popover.Root>
      <Popover.Trigger
        disabled={disabled}
        render={<Button variant="outline" />}
      >
        {label}
      </Popover.Trigger>
      <Popover.Portal>
        <Popover.Positioner
          side={side}
          align={align}
          arrowPadding={arrowPadding}
        >
          <Popover.Popup>
            <Popover.Arrow />
            <Popover.Title>{label}</Popover.Title>
            <Popover.Description>
              You&apos;re all caught up.
            </Popover.Description>
            <div className="mt-3 flex justify-end">
              <Popover.Close render={<Button size="sm" variant="outline" />}>
                Dismiss
              </Popover.Close>
            </div>
          </Popover.Popup>
        </Popover.Positioner>
      </Popover.Portal>
    </Popover.Root>
  );
}

export default function PopoverStory() {
  return (
    <main className="relative h-[520px]">
      {/* Pushed against the left edge with a large arrowPadding: the arrow can't reach the
          trigger centre (data-uncentered). */}
      <div className="absolute top-12 left-0">
        <Example label="Help" arrowPadding={110} />
      </div>
      <div className="absolute top-0 left-[600px]">
        <Example label="Archived" disabled />
      </div>
      {/* Near the left edge: the popup shifts and the arrow is uncentered. */}
      <div className="absolute top-0 left-0">
        <Example label="Notifications" />
      </div>
      <div className="absolute top-40 left-60">
        <Example label="Share" side="right" align="start" />
      </div>
      {/* Near the bottom edge: side="bottom" flips to top. */}
      <div className="absolute bottom-0 left-60">
        <Example label="Filters" />
      </div>
      {/* Room to scroll a trigger out of view (data-anchor-hidden). */}
      <div aria-hidden="true" className="absolute top-full h-[600px] w-px" />
    </main>
  );
}

API reference

Popover.Root

Owns the open state. Renders nothing.

PropTypeDefaultDescription
openboolean—Controlled open state.
defaultOpenbooleanfalseInitial open state.
modalboolean | 'trap-focus'falsetrue: trap focus, inert page, lock scroll.
EventPayloadDescription
onOpenChangebooleanFired when it opens or closes. Angular: the `open` model's change event.
onOpenChangeCompletebooleanFired after the animation finishes.

Popover.Trigger

Toggles the popover. Renders <button>.

Data attributePresent when
data-popup-openopen
data-pressedopen after a pointer press
data-disableddisabled

Popover.Portal

Moves the popup to the end of <body>. Renders <div>.

PropTypeDefaultDescription
keepMountedbooleanfalseKeep in the DOM while closed.

Popover.Positioner

Positions the popup (transform) and exposes size variables. Renders <div> with role presentation.

PropTypeDefaultDescription
side'top' | 'bottom' | 'left' | 'right'"bottom"Preferred side; flips on collision.
align'start' | 'center' | 'end'"center"Alignment along the side.
sideOffsetnumber0Gap from the trigger (styled default 8).
alignOffsetnumber0Shift along the alignment axis.
arrowPaddingnumber5Minimum distance of the arrow from the popup edges.
collisionPaddingnumber5Distance to keep from the viewport edges.
Data attributePresent when
data-openopen
data-closedclosed / animating out
data-anchor-hiddenthe trigger is scrolled out of view
data-alignalways: resolved alignment
data-sidealways: resolved side after flipping
data-instantopened from the keyboard: value "click" (skip the enter animation if wanted)
CSS variableDescription
--available-widthSpace between trigger and viewport edge
--available-heightSpace between trigger and viewport edge
--anchor-widthTrigger width
--anchor-heightTrigger height
--transform-originArrow (or trigger centre) position, for scale animations

Popover.Arrow

Points at the trigger. Renders <div>.

Data attributePresent when
data-openopen
data-closedclosed / animating out
data-uncenteredthe arrow can't reach the trigger centre (clamped by arrowPadding)
data-alignalways: resolved alignment
data-sidealways: resolved side after flipping

Popover.Title

Labels the popup. Renders <h2>.

Popover.Description

Describes the popup. Renders <p>.

Popover.Close

Closes the popover. Renders <button>.

Keyboard

KeyAction
Enter / SpaceOn the trigger: toggle.
EscapeClose and return focus to the trigger.
TabFrom the last element: close and continue to the element after the trigger.
Shift+TabFrom the first element: close and return to the trigger.