Popover

Open a floating panel from a trigger on click or hover.

Popover is a client component that opens a floating panel from a trigger, on click or on hover. Use it for account menus, filters and short help text. Click mode is built on the Headless UI Popover; hover mode uses a custom implementation. Both use @floating-ui/react for positioning and motion/react for the enter and exit animation.

Import#

typescript
import { Popover } from "@reactberry/system/blocks";

Usage#

typescript
import { Popover } from "@reactberry/system/blocks";
import { Box, Button, Text } from "@reactberry/system/elements";

export default function AccountMenu() {
  return (
    <Popover
      trigger={<Text>Account</Text>}
      placement="bottom start"
      panelProps={{ width: "12rem" }}
    >
      {({ close }) => (
        <Box display="flex" flexDirection="column" gap="xxsmall">
          <Button variant="ghost" onClick={close}>
            <Text>Profile</Text>
          </Button>
          <Button variant="ghost" onClick={close}>
            <Text>Sign out</Text>
          </Button>
        </Box>
      )}
    </Popover>
  );
}

Examples#

Hover mode#

Set triggerMode="hover" for help text that opens on pointer hover. hoverDelay sets the delay before opening and closing.

Keyboard shortcuts

Custom trigger#

Use renderTrigger to render your own trigger element instead of the default ghost Button.

API#

Popover#

PropTypeDefaultDescription
children
ReactNode | ((props: { close: () => void; open: boolean; }) => ReactNode)
—

Panel content. The render-function form receives { close, open } and is only called in click mode.

trigger
ReactNode
—

Trigger content. Wrapped in a ghost Button in click mode; rendered directly in hover mode.

placement
string
bottom end

Floating UI placement. Both "bottom end" and "bottom-end" forms are accepted.

triggerProps
any
—

Props spread onto the default trigger Button (click mode only). A ref is merged with the internal refs.

containerProps
any
—

Props spread onto the outer container Box. A ref is merged in hover mode.

panelProps
any
—

Props spread onto the panel, overriding its defaults (width="15rem", p="xsmall", skin="translucent", zIndex="999999").

triggerMode
"hover""click"
"click"

How the panel opens.

hoverDelay
number
200

Delay in ms before opening or closing in hover mode.

usePortal
boolean
false

Renders the panel in a Headless UI Portal and switches positioning from absolute to fixed.

scrollContainer
HTMLElement | null
null

Element whose scroll events close the popover. Click mode falls back to window.

renderTrigger
((props: { ref: (node: HTMLElement | null) => void; onClick?: (() => void); }) => ReactNode)
—

Custom trigger renderer (click mode only). The result is wrapped in a PopoverButton rendered as a div.

onOpenChange
((isOpen: boolean) => void)
—

Called when the open state changes (click mode only).

The panel also gets shape="rounded" and $shadow="medium". Props not listed above are not spread.

Keyboard#

Click mode only; keyboard handling comes from Headless UI Popover.

KeyAction
Enter

Toggles the panel when the trigger has focus.

Space

Toggles the panel when the trigger has focus.

Escape

Closes the panel.

Accessibility#

  • In click mode the trigger is a Headless UI PopoverButton and the panel a PopoverPanel, which manage aria-expanded, aria-controls and focus.
  • Hover mode renders a plain div that opens on mouseenter and closes on mouseleave. It cannot be opened with the keyboard or by touch, so don't put essential content in it.

Notes#

  • Click mode closes on outside click and Escape via Headless UI. It also closes on scroll of scrollContainer or window, except while focus is inside the panel (so focusing an input on mobile does not dismiss it).
  • Hover mode closes on scroll only when scrollContainer is set. When portalled, hovering the panel keeps it open.
  • In hover mode, renderTrigger, triggerProps, and onOpenChange are ignored, and function children are not called.
  • The ref passed to renderTrigger is a no-op; the wrapping PopoverButton receives the positioning ref instead.
  • onOpenChange is called while the popover renders, not in an effect.
  • The panel is offset 4px from the trigger and uses the Floating UI flip and shift (8px padding) middleware.