Skip to content

Popover

Displays rich content in a floating panel anchored to a trigger element.

  • Popup
  • Flyout

Overview

The Popover component displays rich, interactive content in a floating panel that is anchored to a trigger element. Unlike a Tooltip (which shows brief, non-interactive text), a Popover can contain forms, links, buttons, and complex layouts — it is a fully interactive container.

Popovers are the bridge between inline content and full Dialogs. When you need more context than a tooltip but don't want to block the entire page with a modal, a Popover is the right choice.

When to use a Popover:

  • User profile cards triggered by clicking an avatar
  • Filter panels in data-heavy interfaces
  • Color pickers, date selectors, or emoji pickers
  • Confirmation dialogs for inline actions ("Are you sure you want to delete?")
  • Rich formatting toolbars or command palettes

When NOT to use a Popover:

  • For simple text descriptions — use a Tooltip
  • For a list of actions — use a Dropdown Menu
  • For significant content that requires scrolling — use a Drawer or Dialog
  • For critical decisions that must block interaction — use a Dialog with a backdrop

Popovers rely on floating-element positioning libraries (Floating UI, Popper.js) to handle placement, collision detection, and viewport flipping. The floating panel should cast a shadow to establish its elevated position in the visual hierarchy, and open/close transitions should be smooth and purposeful.

Variants

Common Popover Variants

VariantPurposeVisual Treatment
DefaultGeneral-purpose floating container.White/surface background, border, shadow, rounded corners. Optional arrow pointing to trigger.
InformationalRicher alternative to tooltips. Non-interactive or minimally interactive content.Similar to default but may auto-dismiss. Often triggered by hover with a delay.
Form PopoverContains input fields (filter panel, quick-edit).Includes form elements, submit/cancel buttons. Focus trapped when open.
ConfirmationInline confirmation for destructive actions.Compact layout with a question, confirm/cancel buttons. Avoids full-modal weight.
Menu-likeHybrid between popover and dropdown.Structured content that isn't a pure menu but has clickable items.

Placement Options

PlacementDescriptionCommon Use Case
topAbove the trigger, centered.Toolbar buttons, bottom-aligned triggers.
bottomBelow the trigger, centered.Most common default. Dropdowns, select fields.
left / rightBeside the trigger.Sidebar elements, horizontal layouts.
top-start / bottom-end etc.Aligned to trigger edge.Right-to-left aligned content, form fields.

All placements should include flip and shift middleware — if the popover would overflow the viewport, it automatically repositions. Configure shadow depth with the Shadow Tool to signal elevation.

Arrow Variants

  • With arrow: A triangular indicator pointing from the popover to its trigger. Clarifies the anchor relationship. Essential when multiple triggers are close together.
  • Without arrow: Cleaner look, works well when the spatial relationship is obvious (e.g., popover directly below a button).

Properties

Popover Properties

PropertyTypeDefaultDescription
openbooleanfalseControlled open state
onOpenChange(open: boolean) => void—Callback when open state changes
triggerReactNode—The element that anchors and triggers the popover
triggerAction'click' | 'hover' | 'focus' | 'manual''click'How the popover is activated
placementPlacement'bottom'Preferred position relative to trigger
offsetnumber8Distance (px) between popover and trigger
arrowbooleantrueWhether to show the connecting arrow
modalbooleanfalseWhen true, traps focus and renders a backdrop
closeOnOutsideClickbooleantrueDismiss when clicking outside
closeOnEscapebooleantrueDismiss on Escape key
initialFocusRefObject—Element to focus when opened
returnFocusbooleantrueReturn focus to trigger on close
sideOffsetnumber4Additional offset from the trigger edge
collisionPaddingnumber8Minimum distance from viewport edge

Important: When triggerAction is 'hover', include a delay (150–300ms) to prevent accidental triggers, and ensure the popover remains visible while the user moves their cursor from the trigger to the popover content (bridge the gap with a hover-safe zone).

Token Mappings

Design Token Mappings

Token CategoryToken ExamplePopover Usage
Color – Surface--color-surface-elevatedPopover background
Color – Border--color-border-defaultPopover border (1px solid)
Color – Text--color-text-primaryContent text
Shadow--shadow-lgFloating panel elevation. Design with Shadow Tool.
Border Radius--radius-lg (12px)Popover container rounding
Spacing--space-4, --space-5Internal padding
Z-Index--z-popover (50)Stacking order above content, below modals
Transition--duration-normal (200ms)Open/close animation. Preview with Transition Tool.
Max Width--popover-max-width (320px)Prevents excessively wide popovers
Max Height--popover-max-height (400px)Enables scrolling for long content

The shadow depth is critical for popovers. They float above the page surface and need a shadow that communicates this elevation. Use --shadow-lg or higher — a subtle --shadow-sm will make the popover look "stuck" to the page rather than floating above it.

States

Popover States

StateDescriptionVisual Treatment
ClosedPopover is not rendered or is hidden.No DOM presence (or display: none / visibility: hidden).
OpeningTransition from closed to open.Fade in + slight scale (0.95 → 1) or translate from trigger direction. Duration 150–200ms. Use Transition Tool.
OpenPopover is visible and interactive.Full opacity, positioned relative to trigger, shadow visible.
ClosingTransition from open to closed.Reverse of opening animation. Slightly faster (100–150ms).
RepositioningViewport scroll or resize triggers recalculation.Smooth position update via Floating UI's autoUpdate. No visual transition needed — instant repositioning feels more natural.

Focus management states:

  • Non-modal: Focus moves into the popover but is not trapped. Tab can leave the popover, which closes it.
  • Modal: Focus is trapped within the popover. Requires explicit close action (Escape or close button).

Accessibility

Accessibility Requirements

Semantic Structure:

  • The trigger must have aria-haspopup="dialog" (for interactive popovers) or aria-haspopup="true" (for menu-like popovers).
  • The trigger must have aria-expanded="true|false" reflecting the popover's open state.
  • The popover container should have role="dialog" and aria-label or aria-labelledby referencing its title.
  • Use aria-controls on the trigger pointing to the popover's id.

WCAG Compliance:

  • SC 1.4.3 (Contrast – Minimum): All text within the popover must meet 4.5:1 contrast against the popover background. Verify with Contrast Checker.
  • SC 1.4.11 (Non-text Contrast): The popover border or shadow must provide 3:1 contrast against the underlying page, ensuring the popover boundary is perceivable.
  • SC 2.1.1 (Keyboard): The popover must be openable and closable via keyboard. Click-triggered popovers should also open on Enter/Space.
  • SC 2.1.2 (No Keyboard Trap): If the popover is non-modal, Tab must be able to leave the popover (closing it). If modal, Escape must close it.
  • SC 2.4.3 (Focus Order): When opened, focus should move to the first interactive element inside the popover (or the popover itself if no interactive elements). On close, focus must return to the trigger.
  • SC 2.4.7 (Focus Visible): Focus rings must be visible on all interactive elements within the popover.
  • SC 1.4.13 (Content on Hover or Focus): If the popover is hover-triggered, it must remain visible while the pointer is over it, must be dismissible without moving the pointer (Escape), and must persist until dismissed or the information is no longer valid. This is a Level AA requirement often missed.

Hover-Triggered Popovers (SC 1.4.13 compliance): Three requirements that are frequently violated:

  1. Dismissible: User can dismiss without moving pointer (Escape key).
  2. Hoverable: User can move pointer over the popover content without it closing.
  3. Persistent: Content stays visible until user dismisses it, hovers away, or it becomes invalid.

Usage Guidelines

Usage Guidelines

Do:

  • Keep popover content focused and concise. If you need scrolling, consider a Drawer or Dialog.
  • Always provide keyboard access — Enter/Space to open, Escape to close.
  • Use shadows (Shadow Tool) to clearly separate the popover from the page content below.
  • Include a close button (✕) for touch devices and discoverability, even if click-outside also closes.
  • Position the arrow precisely — a misaligned arrow is worse than no arrow.
  • Use portal rendering (React portal or teleporting to <body>) to avoid overflow: hidden clipping from parent containers.

Don't:

  • Don't nest popovers inside popovers. It's disorienting and nearly impossible to manage focus correctly.
  • Don't use popovers for critical information that the user must see — they can be dismissed too easily.
  • Don't auto-open popovers on page load. They should always be user-triggered.
  • Don't use a popover when a Tooltip suffices. If the content is a single line of non-interactive text, it's a tooltip.
  • Don't use a popover with both hover and click triggers simultaneously — this creates confusing behavior on hybrid devices (touch + mouse).
  • Don't forget collision detection. A popover that renders off-screen is worse than no popover at all.

Code Snippets

html
<!-- Popover with Arrow -->
<div class="popover-trigger-wrapper">
  <button class="btn btn-secondary"
          aria-haspopup="dialog"
          aria-expanded="false"
          aria-controls="profile-popover"
          id="profile-trigger">
    View Profile
  </button>

  <div id="profile-popover"
       class="popover"
       role="dialog"
       aria-labelledby="popover-title"
       hidden>
    <div class="popover-arrow"></div>
    <div class="popover-content">
      <h4 id="popover-title" class="popover-title">Jane Smith</h4>
      <p class="popover-text">Senior Designer · San Francisco</p>
      <div class="popover-actions">
        <button class="btn btn-primary btn-sm">Message</button>
        <button class="btn btn-ghost btn-sm">View full profile</button>
      </div>
    </div>
    <button class="popover-close" aria-label="Close">
      <svg aria-hidden="true"><!-- close icon --></svg>
    </button>
  </div>
</div>

<!-- Confirmation Popover -->
<div class="popover-trigger-wrapper">
  <button class="btn btn-destructive btn-sm"
          aria-haspopup="dialog"
          aria-expanded="false"
          aria-controls="confirm-popover">
    Delete
  </button>

  <div id="confirm-popover" class="popover popover--confirmation" role="dialog"
       aria-labelledby="confirm-title" hidden>
    <h4 id="confirm-title">Delete this item?</h4>
    <p>This action cannot be undone.</p>
    <div class="popover-actions">
      <button class="btn btn-ghost btn-sm">Cancel</button>
      <button class="btn btn-destructive btn-sm">Delete</button>
    </div>
  </div>
</div>
tsx
// Popover using Radix UI Primitives
import * as PopoverPrimitive from '@radix-ui/react-popover';

interface PopoverProps {
  trigger: React.ReactNode;
  children: React.ReactNode;
  side?: 'top' | 'bottom' | 'left' | 'right';
  align?: 'start' | 'center' | 'end';
  sideOffset?: number;
  showArrow?: boolean;
  modal?: boolean;
}

function Popover({
  trigger,
  children,
  side = 'bottom',
  align = 'center',
  sideOffset = 8,
  showArrow = true,
  modal = false,
}: PopoverProps) {
  return (
    <PopoverPrimitive.Root modal={modal}>
      <PopoverPrimitive.Trigger asChild>
        {trigger}
      </PopoverPrimitive.Trigger>
      <PopoverPrimitive.Portal>
        <PopoverPrimitive.Content
          side={side}
          align={align}
          sideOffset={sideOffset}
          className="popover-content"
          collisionPadding={8}
        >
          {children}
          {showArrow && (
            <PopoverPrimitive.Arrow className="popover-arrow" />
          )}
          <PopoverPrimitive.Close className="popover-close" aria-label="Close">
            <CloseIcon />
          </PopoverPrimitive.Close>
        </PopoverPrimitive.Content>
      </PopoverPrimitive.Portal>
    </PopoverPrimitive.Root>
  );
}

// Usage: Profile Card
<Popover
  trigger={<button className="avatar-btn"><Avatar src={user.photo} /></button>}
  side="bottom"
  align="start"
>
  <div className="profile-card">
    <h4>{user.name}</h4>
    <p>{user.role} · {user.location}</p>
    <Button variant="primary" size="sm">Message</Button>
  </div>
</Popover>

// Usage: Confirmation Popover
<Popover
  trigger={<Button variant="destructive" size="sm">Delete</Button>}
  side="top"
  modal
>
  <h4>Delete this item?</h4>
  <p>This action cannot be undone.</p>
  <div className="popover-actions">
    <PopoverPrimitive.Close asChild>
      <Button variant="ghost" size="sm">Cancel</Button>
    </PopoverPrimitive.Close>
    <Button variant="destructive" size="sm" onClick={handleDelete}>
      Confirm Delete
    </Button>
  </div>
</Popover>

Design Systems

Radix UI provides a comprehensive Popover primitive with Root, Trigger, Anchor, Portal, Content, Close, and Arrow compound components. Key props include side, sideOffset, align, alignOffset, collisionPadding, avoidCollisions, sticky, and hideWhenDetached. Radix handles focus management, Escape dismissal, click-outside dismissal, and portal rendering out of the box. The modal prop toggles between non-modal (focus can leave) and modal (focus trapped) behavior. Radix uses Floating UI internally for positioning.

Headless UI provides a Popover component with Popover.Button (trigger), Popover.Panel (content), Popover.Overlay (optional backdrop), and Popover.Group (for coordinating multiple popovers). Headless UI's Popover.Group is unique — it allows only one popover in the group to be open at a time, with focus moving between panels. Transition support is built in via Transition component wrapping Popover.Panel. Configure transition timing with the Transition Tool.

Material Design 3 uses the Popover from MUI with anchorEl, open, onClose, anchorOrigin (which corner of the anchor to attach to), transformOrigin (which corner of the popover aligns to the anchor), elevation (shadow depth), marginThreshold (viewport margin), and TransitionComponent (defaults to Grow). MUI's approach is anchor-origin based rather than side-based, which offers more control but is less intuitive than Radix/Floating UI's side/align API.

Ant Design provides Popover with title, content, trigger ('hover' | 'click' | 'focus' | 'contextMenu'), placement (12 positions), arrow (true | false | { pointAtCenter: true }), open (controlled), overlayClassName, overlayStyle, and getPopupContainer. Ant extends its Tooltip component under the hood, adding support for richer content. The getPopupContainer prop solves the common overflow: hidden clipping issue by letting you specify the portal target.

Chakra UI provides Popover with PopoverTrigger, PopoverContent, PopoverHeader, PopoverBody, PopoverFooter, PopoverArrow, and PopoverCloseButton. Chakra supports trigger="hover" and trigger="click", placement (all Popper.js positions), closeOnBlur, closeOnEsc, initialFocusRef, and returnFocusOnClose. Chakra uses Popper.js v2 for positioning with automatic flipping and boundary detection. The compound component API with semantic sections (Header/Body/Footer) encourages consistent internal structure.

Use the Shadow Tool to design the popover's elevation shadow and the Transition Tool to configure open/close animations across implementations.

OverlayPopupFlyout