Loading…
Loading…
Displays rich content in a floating panel anchored to a trigger element.
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:
When NOT to use a Popover:
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.
| Variant | Purpose | Visual Treatment |
|---|---|---|
| Default | General-purpose floating container. | White/surface background, border, shadow, rounded corners. Optional arrow pointing to trigger. |
| Informational | Richer alternative to tooltips. Non-interactive or minimally interactive content. | Similar to default but may auto-dismiss. Often triggered by hover with a delay. |
| Form Popover | Contains input fields (filter panel, quick-edit). | Includes form elements, submit/cancel buttons. Focus trapped when open. |
| Confirmation | Inline confirmation for destructive actions. | Compact layout with a question, confirm/cancel buttons. Avoids full-modal weight. |
| Menu-like | Hybrid between popover and dropdown. | Structured content that isn't a pure menu but has clickable items. |
| Placement | Description | Common Use Case |
|---|---|---|
| top | Above the trigger, centered. | Toolbar buttons, bottom-aligned triggers. |
| bottom | Below the trigger, centered. | Most common default. Dropdowns, select fields. |
| left / right | Beside 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.
| Property | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Controlled open state |
onOpenChange | (open: boolean) => void | — | Callback when open state changes |
trigger | ReactNode | — | The element that anchors and triggers the popover |
triggerAction | 'click' | 'hover' | 'focus' | 'manual' | 'click' | How the popover is activated |
placement | Placement | 'bottom' | Preferred position relative to trigger |
offset | number | 8 | Distance (px) between popover and trigger |
arrow | boolean | true | Whether to show the connecting arrow |
modal | boolean | false | When true, traps focus and renders a backdrop |
closeOnOutsideClick | boolean | true | Dismiss when clicking outside |
closeOnEscape | boolean | true | Dismiss on Escape key |
initialFocus | RefObject | — | Element to focus when opened |
returnFocus | boolean | true | Return focus to trigger on close |
sideOffset | number | 4 | Additional offset from the trigger edge |
collisionPadding | number | 8 | Minimum 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 Category | Token Example | Popover Usage |
|---|---|---|
| Color – Surface | --color-surface-elevated | Popover background |
| Color – Border | --color-border-default | Popover border (1px solid) |
| Color – Text | --color-text-primary | Content text |
| Shadow | --shadow-lg | Floating panel elevation. Design with Shadow Tool. |
| Border Radius | --radius-lg (12px) | Popover container rounding |
| Spacing | --space-4, --space-5 | Internal 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.
| State | Description | Visual Treatment |
|---|---|---|
| Closed | Popover is not rendered or is hidden. | No DOM presence (or display: none / visibility: hidden). |
| Opening | Transition from closed to open. | Fade in + slight scale (0.95 → 1) or translate from trigger direction. Duration 150–200ms. Use Transition Tool. |
| Open | Popover is visible and interactive. | Full opacity, positioned relative to trigger, shadow visible. |
| Closing | Transition from open to closed. | Reverse of opening animation. Slightly faster (100–150ms). |
| Repositioning | Viewport 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:
Semantic Structure:
aria-haspopup="dialog" (for interactive popovers) or aria-haspopup="true" (for menu-like popovers).aria-expanded="true|false" reflecting the popover's open state.role="dialog" and aria-label or aria-labelledby referencing its title.aria-controls on the trigger pointing to the popover's id.WCAG Compliance:
Hover-Triggered Popovers (SC 1.4.13 compliance): Three requirements that are frequently violated:
Do:
portal rendering (React portal or teleporting to <body>) to avoid overflow: hidden clipping from parent containers.Don't:
<!-- 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>// 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>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.