Introducing PrimeReact v11-alpha 🎉Discover Now

usePopover

Hook that manages popover open state, outside click dismissal, escape key handling, and focus management.

basic-demo

Usage#

import { usePopover } from '@primereact/headless/popover';
import { usePortal } from '@primereact/headless/portal';
import { usePositioner } from '@primereact/headless/positioner';
import { createPortal } from 'react-dom';
const { triggerProps, popupProps, positionerProps, arrowProps, closeProps, state } = usePopover();
const portal = usePortal();
 
usePositioner({
    anchor: state.anchorElement,
    content: state.positionerElement,
    arrow: state.arrowElement,
    side: 'bottom',
    flip: true,
    shift: true
});

usePopover manages open/close state, outside click dismissal, escape key handling, and optional focus trapping. Use usePositioner for anchor-relative positioning and usePortal with createPortal for body portaling. See Primitive for a component-based API.

Features#

  • Open/close lifecycle — controlled or uncontrolled state with outside-click dismissal and escape-key handling
  • Focus management — optional focus trap plus autoFocus control over initial focus when the popup mounts
  • Anchor refs — exposes state.anchorElement, state.positionerElement, and state.arrowElement for wiring into usePositioner
  • Arrow support — arrowProps pairs with the positioner's CSS custom properties for per-side arrow placement
  • Portal-friendly — state.open drives conditional rendering of portaled content without managing mount state yourself
  • Imperative controls — setOpen(open, event) for programmatic toggling tied to a source event

Working with callbacks#

Controlled open state#

Pass open and onOpenChange to drive visibility from external state.

const [isOpen, setIsOpen] = React.useState(false);
 
const popover = usePopover({
    open: isOpen,
    onOpenChange: (e) => setIsOpen(e.open)
});
 
<>
    <button {...triggerProps}></button>;
    {
        portal.state.mounted &&
            state.open &&
            createPortal(
                <div {...positionerProps}>
                    <div {...popupProps}>
                        <div {...arrowProps} />
                        <button {...closeProps}></button>
                    </div>
                </div>,
                document.body

Composing with usePositioner#

usePopover manages state and element refs but not positioning. Feed the element refs from state into usePositioner to place the popup relative to the trigger.

const { triggerProps, popupProps, positionerProps, state } = usePopover();
 
usePositioner({
    anchor: state.anchorElement,
    content: state.positionerElement,
    side: 'bottom',
    flip: true,
    shift: true
});

Rendering into a portal#

Gate the portal content on both portal.state.mounted and state.open so the popup renders only on the client and only when visible.

const portal = usePortal();
 
{
    portal.state.mounted &&
        state.open &&
        createPortal(
            <div {...positionerProps}>
                <div {...popupProps}>...</div>
            </div>,
            document.body
        );
}

Arrow positioning#

Spread arrowProps on an arrow element and pass state.arrowElement to the positioner. The positioner writes --placer-arrow-x and --placer-arrow-y for you to consume in CSS.

<div {...popupProps}>
    <div {...arrowProps} />
</div>
[data-scope='popover'][data-part='arrow'] {
    position: absolute;
    width: 0.5rem;
    height: 0.5rem;
    border-left: 1px solid var(--p-content-border-color);
    border-top: 1px solid var(--p-content-border-color);
}
[data-part='arrow'][data-side='bottom'] {
    top: -0.25rem;
    left: var(--px-placer-arrow-x);
    transform: translateX(-50%) rotate(45deg);
}
[data-part='arrow'][data-side='top'] {
    bottom: -0.25rem;
    left: var(--px-placer-arrow-x);
    transform: translateX(-50%) rotate(225deg);
}

Enable trapped for popovers that should behave like a mini-dialog, keeping keyboard focus inside until dismissed.

const popover = usePopover({ trapped: true });

Styling with data attributes#

The hook exposes state through data-* attributes on each part. Use them as CSS selectors — no className juggling.

ScopePartStates
popovertriggerdata-positioner-open
popoverpopupdata-open, data-closed
popoverarrowdata-side
[data-scope='popover'][data-part='trigger'][data-positioner-open] {
    background-color: light-dark(var(--p-surface-100), var(--p-surface-700));
}
[data-scope='popover'][data-part='popup'][data-open] {
    opacity: 1;
}

API#

usePopover#

NameTypeDefault
anchorHTMLElementundefined
External anchor element for positioning the popover programmatically. When provided, it is used instead of the trigger element.
defaultOpenbooleanundefined
Whether the popover is open by default.
openbooleanundefined
Whether the popover is open.
trappedbooleanfalse
When enabled, focus is trapped within the popover (modal behavior). When disabled, focus leaving the popover closes it (non-modal behavior).
autoFocusbooleantrue
Whether to focus the first focusable element when the popover is opened.
closeOnEscapebooleantrue
Specifies if pressing escape key should hide the dialog.
onOpenChange(event: usePopoverOpenChangeEvent) => voidundefined
Callback to invoke when the open state changes.
onExitComplete() => voidundefined
Callback fired after the leave (exit) transition completes.
appendTo"body" | HTMLElement | "self"'body'
DOM element or CSS selector to append the overlay to.

Accessibility#

Escape dismisses the popover, focus moves into the content when opened, and Tab cycles focusable children. See Primitive for full WAI-ARIA compliance details.