Popover
Primitive

Popover

Positioned disclosure content with optional anchor, modal behavior, focus guards, arrow geometry, and close controls.

Live behavior

Popover in motion

Interactive
Atom behavior · App-owned appearance

Open the non-modal layer and dismiss it with Escape.

waiting for input

When to Use

Use Popover for a small interactive panel that belongs to a trigger, such as quick actions or compact settings. Use Tooltip for short, non-interactive help, HoverCard for preview content, and Dialog when the user must focus on a larger task. A popover should add helpful choices without becoming a whole page inside a floating box.

Features

The behavior Atom owns before your product adds appearance.

  • Controlled and uncontrolled open state.
  • Click or hover trigger mode with opening-reason tracking; pointer hover never moves focus.
  • Visible Title and Description parts with generated accessible relationships.
  • Configurable interaction-aware initial and final focus, including a touch-safe Content default and outside-dismissal focus preservation.
  • Optional anchor separate from the trigger.
  • Floating UI positioning that preserves centered alignment by shifting within the viewport first. Edge-aligned content resolves alternate alignments before shifting, and both modes retain opposite- and perpendicular-side fallbacks; Arrow coordinates follow the resolved result.
  • Modal mode with focus trap, background isolation, and scroll lock.
  • Non-modal focus guards and layer-aware completed-activation outside dismissal with a preventable consumer event.
  • Portalled Atom controls opened by content, including Dropdown Menu, remain inside the Popover interaction and focus boundary.
  • Stack-aware Escape dismissal for nested overlays.
  • Close button part and portal support.

Import

tsx
import { Popover } from "@flowstack-ui/atom";

Anatomy

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

API Reference

Root

Owns open state, dismissal rules, trigger mode, and the references used to position Content. Root renders no DOM element.

PropTypeDefault
openboolean-
defaultOpenbooleanfalse
onOpenChange(open: boolean, reason?: PopoverCloseReason) => void-
modalbooleanfalse
triggerMode"click" | "hover""click"
openDelaynumber200
closeDelaynumber300
closeOnEscapebooleantrue
closeOnInteractOutsidebooleantrue
disabledbooleanfalse

ARIA: Root renders no element and adds no ARIA attributes.

Data attributes: Root renders no element and exposes no data attributes.

Anchor

Optional positioning reference. Use it when content should be positioned relative to a different element than the trigger.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-

ARIA: Anchor adds no roles or ARIA attributes.

Data attributeValues
[data-slot]"popover-anchor"

Trigger

Opens or toggles the popover and connects the button to Content. In hover mode, it also starts the configured open and close delays.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-
ARIA attributeValues
role"button" for a custom rendered element
aria-haspopup"dialog"
aria-expandedCurrent open state
aria-controlsContent ID while open
aria-disabledtrue when Root is disabled
Data attributeValues
[data-slot]"popover-trigger"
[data-state]"open" | "closed"
[data-trigger-mode]"click" | "hover"
[data-disabled]Present when disabled

Portal

Moves Content to another DOM container so it can escape clipping and stacking ancestors. It renders no wrapper.

PropTypeDefault
containerHTMLElement | nulldocument.body after mount
disabledbooleanfalse

ARIA: Portal adds no roles or ARIA attributes.

Data attributes: Portal renders no wrapper and exposes none.

Content

Renders the positioned dialog, manages outside dismissal, and manages focus. Modal Content traps focus, makes background subtrees inert through Atom's stacked modal-layer system, and locks document scrolling; non-modal Content does none of those things and closes when focus leaves its trigger/content scope. Closing or unmounting modal Content restores author-provided background state. Non-Arrow children render inside [data-slot="popover-viewport"]; a direct Arrow remains its sibling so styled layers can scroll the viewport without clipping the pointer. Content exposes measured --atom-floating-available-width and --atom-floating-available-height properties. Portalled Content preserves an explicit dir; otherwise it resolves direction from the mounted Anchor/Trigger and then Direction.Provider.

PropTypeDefault
side"top" | "right" | "bottom" | "left""bottom"
align"start" | "center" | "end""center"
sideOffsetnumber8
initialFocusPopoverFocusTarget<PopoverInitialFocusDetails>safe interaction-aware target
finalFocusPopoverFocusTarget<PopoverFinalFocusDetails>prior valid target, then Trigger
onInteractOutside(event: OutsideInteractionEvent) => void-

onInteractOutside runs before dismissal. Calling its preventDefault() method keeps Content open without cancelling the original destination click. Only the topmost registered layer receives an outside activation; dragged, cancelled, secondary-button, and multi-pointer sessions do not dismiss. Portalled Atom layers whose trigger is inside Content remain part of the same interaction boundary, so opening a Dropdown Menu or another controlled layer does not dismiss the Popover. When a browser temporarily moves focus to the document while activating a nested modal layer, Popover waits for that focus transaction to settle and preserves the parent boundary while its controlled portalled child remains open. This includes transient focus reported on the document body or root element by browsers while background isolation changes. While that child is open, it owns the active focus transaction; the parent Popover resumes ordinary focus-out dismissal after the child closes.

ARIA attributeValues
role"dialog"
aria-labelNative explicit value
aria-labelledbyNative value or generated Title relationship
aria-describedbyNative value or generated Description relationship
aria-modaltrue in modal mode
Data attributeValues
[data-slot]"popover-content"
[data-state]"open" | "closed"
[data-side]"top" | "right" | "bottom" | "left"
[data-positioned]Present after the first positioning frame

Native aria-label, aria-labelledby, and aria-describedby pass through. Explicitly passing aria-describedby={undefined} suppresses the generated description relationship. initialFocus and finalFocus accept an element ref, a callback receiving interaction/reason details, or false to suppress that automatic operation.

Atom identifies Content as the allowed modal scroll region, but remains headless: consumers set its maximum size and scrolling styles. For long content, constrain [data-slot="popover-viewport"], apply overflow: auto, and choose any desired overscroll-behavior. If another library portals an interactive child, target a container rendered inside Content so it remains on the modal's owned DOM path.

Title

Renders the visible heading that names Content. It defaults to h2, accepts as="h1" through as="h6", forwards heading props/ref, and registers its stable ID with Content.

PropTypeDefault
as"h1" | "h2" | "h3" | "h4" | "h5" | "h6""h2"
Data attributeValues
[data-slot]"popover-title"

Description

Renders a visible paragraph that describes Content and registers its stable ID for the generated aria-describedby relationship.

Data attributeValues
[data-slot]"popover-description"

Close

Renders a button that requests Root to close after any consumer click handler runs without preventing the event. Close records keyboard, mouse, pen, or touch activation so final-focus callbacks receive accurate details.

PropTypeDefault
asChildbooleanfalse
renderRenderProp-

ARIA: Close uses native button semantics. Give icon-only controls an aria-label.

Data attributeValues
[data-slot]"popover-close"

Arrow

Renders a decorative SVG pointer using the actual side chosen after collision handling. getPopoverArrowGeometry exposes the same geometry for custom arrows.

PropTypeDefault
widthnumber10
heightnumber5
asChildbooleanfalse
renderRenderProp-

ARIA: Arrow is decorative and hidden from assistive technology.

Data attributeValues
[data-slot]"popover-arrow"
[data-side]"top" | "right" | "bottom" | "left"

Advanced compound parts can read usePopoverContext or usePopoverContentContext. Their matching context providers are also public for low-level composition. getPopoverArrowGeometry returns the SVG geometry used by Arrow for a given side, width, and height.

Examples

Basic Popover

tsx
import { Popover } from "@flowstack-ui/atom";

export default function ActionsPopover() {
  return (
    <Popover.Root>
      <Popover.Trigger>Actions</Popover.Trigger>
      <Popover.Portal>
        <Popover.Content>
          <Popover.Title>Project actions</Popover.Title>
          <Popover.Description>
            Choose one compact action for this project.
          </Popover.Description>
          <button type="button">Duplicate</button>
          <Popover.Close>Done</Popover.Close>
          <Popover.Arrow />
        </Popover.Content>
      </Popover.Portal>
    </Popover.Root>
  );
}

Custom Anchor

tsx
import { Popover } from "@flowstack-ui/atom";

export default function AnchoredPopover() {
  return (
    <Popover.Root>
      <Popover.Anchor asChild>
        <span>Account</span>
      </Popover.Anchor>
      <Popover.Trigger>Open account actions</Popover.Trigger>
      <Popover.Portal>
        <Popover.Content side="right" align="start">
          <Popover.Title>Account actions</Popover.Title>
          <button type="button">View profile</button>
          <Popover.Close>Close</Popover.Close>
        </Popover.Content>
      </Popover.Portal>
    </Popover.Root>
  );
}

Accessibility

Popover uses the WAI-ARIA dialog pattern for Content and an aria-haspopup="dialog" trigger. Render one visible Popover.Title or provide native aria-label/aria-labelledby. Description connects automatically when rendered. Popover intentionally exposes no camel-case ariaLabel alias.

Intentional keyboard, mouse, and pen openings focus an explicit initialFocus, native autoFocus, the first available descendant, or Content. Touch defaults to Content so opening does not unexpectedly raise a virtual keyboard. Pointer-hover opening never moves focus. Escape and Close restore a valid explicit finalFocus, the prior element, or Trigger; outside pointer and focus dismissal preserve the destination. Touch and pen outside interactions dismiss only after resolving as taps; movement, scrolling, and pointer cancellation keep the Popover open. In modal mode, focus remains contained inside the popover scope, including registered portalled layers opened by descendants.

KeyDescription
EnterToggles a non-native trigger.
SpaceToggles a non-native trigger.
EscapeCloses the topmost popover when closeOnEscape is enabled.
TabIn modal mode, focus remains trapped inside content. In non-modal mode, focus guards close the popover when tabbing away.

Changelog

Unreleased

0.24.0

  • Added public Agent Knowledge for component selection, required composition, recurring mistakes, and validation.
  • Preserved centered Content alignment near viewport edges by shifting it into view before considering alternate placements.
  • Kept Popover open when focus or pointer interaction moves into any portalled Atom controlled layer opened from its Content, including Dropdown Menu, even when the browser briefly moves focus to the document during modal setup.

0.20.3

  • Modal Content now inherits document-only overflow locking so sticky application chrome remains anchored at nonzero page scroll positions.

0.20.1

  • Made modal Content participate in Atom's stacked background-isolation system and restore background inert state on close or unmount.
  • Clarified consumer ownership of scroll dimensions, overscroll styling, and third-party portal containers.

0.20.0

  • Added preventable Content.onInteractOutside and moved outside dismissal to the shared layer-aware completed-activation contract.

0.6.10

  • Distinguished outside touch and pen taps from scroll gestures so scrolling no longer dismisses an open Popover; mouse outside dismissal remains immediate.

0.6.9

  • Preserved resolved trigger/anchor or provider direction on portalled Content, while retaining explicit Content dir precedence.

0.6.8

  • Added an internal [data-slot="popover-viewport"] around non-Arrow Content children so constrained content can scroll without clipping the Arrow.
  • Exposed measured available dimensions through headless floating properties.

0.6.7

  • Modal Popover now inherits root/body overflow locking without fixed-body repositioning or unlock-time scroll restoration, avoiding iOS Safari browser-toolbar flicker while retaining focus and scroll containment.

0.6.3

  • Prioritized every usable alignment on the requested side, followed by the opposite side, before allowing perpendicular-axis collision fallbacks.

0.6.2

  • Added perpendicular-side collision fallbacks after the preferred and opposite sides so constrained popovers can resolve onto the axis with available room.

0.4.0

  • Added visible Title and Description parts with generated, hydration-stable aria-labelledby and aria-describedby relationships.
  • Standardized naming on native aria-label, aria-labelledby, and aria-describedby; removed the custom ariaLabel alias.
  • Added interaction-aware initialFocus and finalFocus targets, touch-safe Content focus, hover-without-focus-steal, dismissal reasons, and outside-destination preservation.

0.3.4

  • Fixed modal Popover scroll locking to avoid duplicate body-padding compensation when the document already preserves its scrollbar gutter.

0.3.1

  • Fixed exit-presence cleanup so closed Popover Content unmounts after its CSS motion window even when no end event is emitted.

0.2.0

  • Fixed Popover positioning when Anchor uses its default display: contents wrapper by resolving the usable child element as the Floating UI reference and refreshing the reference after refs commit.
  • Fixed non-modal and modal Popover dismissal so clicks and focus movement inside nested portalled Popover layers do not close the parent Popover.
  • Added shared dismissable layer Escape handling so nested overlays close before parent Popover layers.
  • Added scoped modal focus containment for modal Popover and registered Popover content with parent modal focus scopes when nested inside another modal primitive.
  • Removed redundant role="button" and tabIndex={0} from the default native button trigger path.

0.1.0

  • Initial Atom release.