Skip to content

Popover

Floating content anchored to a trigger element. Supports click-to-open (popover) and hover (infotip) modes with built-in 4-side positioning, automatic flip, shift, and viewport collision detection without a third-party positioning dependency.

Preview

Installation

$pnpm exec dgadd add ui/popover
[Installs to]src/components/ui/popover[Item]ui/popover

dgadd is not public on npm yet. Until the first release, pack @diffgazer/add from the repository and install that tarball into this app, which is what puts dgadd on pnpm exec.

UI components require Tailwind CSS v4. Local copy mode imports src/styles/styles.css; package mode uses @diffgazer/ui CSS once packages are available.

Package-mode consumers must install the required @diffgazer/keys peer alongside @diffgazer/ui. Copy and dgadd installs rewrite the autofocus helper to local source.

Usage

tsx
"use client";import { Popover } from "@/components/ui/popover";export default function PopoverBasicExample() {  return (    <Popover>      <Popover.Trigger>        {(triggerProps) => (          <button            {...triggerProps}            type="button"            className="border border-foreground/30 px-3 py-1 font-mono text-sm focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-0"          >            click me          </button>        )}      </Popover.Trigger>      <Popover.Content role="dialog" aria-label="Popover actions" className="p-4 font-mono text-sm">        <p className="text-foreground">Popover content with interactive elements.</p>        <button          type="button"          className="mt-2 border border-foreground/30 px-2 py-0.5 text-xs focus-visible:outline-2 focus-visible:outline-ring focus-visible:outline-offset-0"        >          action        </button>      </Popover.Content>    </Popover>  );}

Examples

Hover Mode

Preview

Placement

Preview

Actions menu

Preview

Controlled

Preview

API Reference

Popover

NameTypeDefaultDescription
openbooleanControlled open state. Pair with onOpenChange.
defaultOpenbooleanfalseInitial open state for uncontrolled mode.
onOpenChange(open: boolean) => voidFired when the open state changes.
triggerMode"click" | "hover""click"Click toggles; hover delays pointer-open, keyboard focus opens immediately, and leave closes.
popupRole"dialog" | "menu" | "listbox" | "tree" | "grid"Overrides the aria-haspopup value applied to the trigger. Hover-mode tooltip content is selected by triggerMode, not popupRole.
enabledbooleantrueWhen false, the popover never opens and trigger handlers are no-ops.
delayMsnumber500Hover mode only. Delay before opening after pointer enter; keyboard focus opens immediately.
closeDelayMsnumber150 (hover) | 0 (click)Delay before closing after hover/focus leaves the trigger or content.
childrenrequiredReactNodePopover.Trigger and Popover.Content subparts.

Popover.Trigger

NameTypeDefaultDescription
childrenrequiredReactNode | (props: PopoverTriggerRenderProps) => ReactNodeTrigger element. Pass a single element (cloned with merged ARIA/handlers), text (wrapped in <button>), or a render function for full control.

Popover.Content

NameTypeDefaultDescription
role"dialog" | "menu" | "listbox" | "tree" | "grid" | "tooltip"Popup ARIA role. Defaults to "tooltip" in hover mode. A role="dialog" popup gains a fallback accessible name and is focusable when aria-label and aria-labelledby are both missing.
side"top" | "bottom" | "left" | "right""bottom"Preferred side relative to the trigger.
align"start" | "center" | "end""center"Alignment along the chosen side.
sideOffsetnumber6Pixel gap from the trigger along the side axis.
alignOffsetnumber0Pixel offset along the alignment axis.
avoidCollisionsbooleantrueFlips to the opposite side, then cross-axis sides, then shifts within the viewport.
collisionPaddingnumber8Minimum gap between the content and the viewport edge during collision avoidance.
autoFocusbooleantrueDialog and menu roles only. When true, focuses the first focusable child on open (or the content itself for role="dialog" without a focusable child).
childrenrequiredReactNodePopover body content.

Data attributes

AttributeApplies toValuesDescription
data-statePopover.Content"open" | "closed"Presence state forwarded by FloatingPanel for enter/exit animation styling.
data-sidePopover.Content"top" | "right" | "bottom" | "left"Resolved side after collision handling.
data-alignPopover.Content"start" | "center" | "end"Resolved alignment after collision handling.
data-positionedPopover.Contentpresent after first measurementMarks content that has measured and can animate from its resolved origin.

Accessibility

Keyboard Navigation

Click-mode triggers toggle with pointer or keyboard activation. Escape closes open content and returns focus to the trigger. Dialog and menu content can auto-focus on open. Tab follows the browser's normal order; the popover stays open while focus moves between its trigger and content, then closes when focus leaves both.

KeyAction
Enter / SpaceToggles the trigger in click mode.
EscapeCloses open click-mode content and returns focus to the trigger.
Tab / Shift+TabMoves normally and closes click-mode content only after focus leaves the trigger/content pair.

Notes

Requires @diffgazer/keys (package mode)

Popover's auto-focus on open (focusing the first focusable element in the content) imports from the required @diffgazer/keys peer. Package consumers must install @diffgazer/keys with @diffgazer/ui; packages are not yet published to npm, so until the first release install both from locally packed tarballs. Importing @diffgazer/ui/components/popover without keys fails at module load with an error naming the missing @diffgazer/keys package. Copy/dgadd consumers do not need the package — copy mode rewrites the focusable helper to local source.

Trigger Modes

Set `triggerMode="click"` (default) for interactive popovers that toggle on click. Set `triggerMode="hover"` for tooltip-like behavior with delay. Hover mode renders with `role="tooltip"` and content stays open while the pointer hovers it.

Positioning

Content is positioned relative to the trigger with `side` (top/bottom/left/right), `align` (start/center/end), `sideOffset`, and `alignOffset`. When `avoidCollisions` is true (default), content flips to the opposite side if it would overflow, then tries cross-axis sides, then shifts within the viewport.

Click Mode

In click mode, the popover toggles on trigger click, dismisses on outside click, Escape, or focus leaving the trigger/content pair. Content is interactive (pointer-events enabled). Use for forms, menus, or rich content.

Popup Role Contract

popupRole controls the trigger aria-haspopup value and accepts dialog, menu, listbox, tree, or grid. Hover mode content renders role="tooltip" automatically; tooltip is not a popupRole value. Consumers remain responsible for matching the role to the content pattern and supplying an accessible name for dialog-like content.

Controlled

Use `open` and `onOpenChange` props for controlled state. Works with both trigger modes.

Default Surface

Popover.Content ships the surface: a 1px --border hairline, a --surface-1 fill with a 1px --surface-1-highlight inner top lip, and the family's tight rounded-sm corners. --surface-1 sits one step off the page background (lighter in dark, darker in light), so a floating layer separates from dense content by depth step rather than by blur — there is still no drop shadow. There is no padding either, so menu-style content can sit flush. `className` merges last, so a consumer can override or drop any of it; FloatingPanel underneath stays fully headless.

Actions Menu

For a menu button, set popupRole="menu", give Popover.Content align="start" and no padding, and let Menu autoFocus with onClose closing the popover and onSelect closing it before running the pick; the popover then hands focus back to the trigger, whose aria-expanded carries the open state (a trigger sitting in a roving-focus row should drop its own highlight ring while open). MenuItem hotkey is a label only — bind the key in Menu onKeyDown, keep an entry the state cannot run listed and disabled with its reason instead of hiding it, and put the destructive entry last behind a MenuDivider. Inside the open menu ArrowUp/ArrowDown move the highlight and ArrowLeft/ArrowRight are inert (they only serve submenus); Escape and Tab close it. See the "Actions menu" example.

Portal Rendering

Content renders through the shared Portal primitive. When a PortalContainerProvider is present, Popover.Content uses that scoped container; otherwise it falls back to document.body. This keeps nested overlay trees in the same portal scope while still escaping overflow:hidden ancestors by default.

When role="dialog" is used without aria-label or aria-labelledby, the content falls back to the accessible name "Popover" so assistive technology never sees an unlabeled dialog. Provide your own label for any production usage; the fallback exists to prevent silent accessibility regressions during development.

Source

Install via CLI: pnpm exec dgadd add ui/popover. Keyboard hooks are included as standalone copies. For the full experience, use --integration keys.

Highlighted source loads after this disclosure opens. Browse the source repository.