Popover
A floating panel anchored to a trigger that resizes smoothly with its content and follows the trigger’s direction.
pnpm dlx shadcn@latest add https://hextaui.com/r/popover.jsonAdds the component, the HextaUI theme tokens and any HextaUI components it depends on.
Add the theme tokens to your global CSS, if you haven’t yet.
Install the dependencies.
pnpm add @base-ui/react cnCopy and paste the following code into your project.
components/ui/popover.tsx Update the import paths to match your project setup.
Content that changes size
When the content grows or shrinks, the popup animates its height instead of jumping. Continuous changes, like typing, follow the content directly so nothing lags behind.
Controlled
Pass open and onOpenChange to drive it from your own state. The second argument tells you why it changed, such as trigger-press, outside-press or escape-key.
Open: false · Last reason: none
Placement
side and align set the preferred position. When there is no room, the popup flips to the other side and shifts to stay on screen, keeping 8px from the edges.
Open on hover
Set openOnHover on the trigger for preview cards. delay and closeDelay keep it from flickering as the pointer passes over.
Detached triggers
Create a handle with createPopoverHandle to share one popover between several triggers anywhere in the tree. Each trigger passes a payload, and the popup renders it through a function child.
With a calendar
Use className="w-auto p-0" to fit content that brings its own padding. The popup follows the calendar as it changes months.
Nested
A popover inside another popover or a sheet layers above its parent. Clicks inside the child keep the parent open, and Escape closes only the topmost layer.
Long content
Unbroken text wraps inside the popup. When the content is taller than the space available, the popup scrolls inside instead of running off screen.
Modal
With modal, page scroll is locked and outside clicks only dismiss the popover. Render a <PopoverClose /> inside so focus can be trapped and touch screen readers have a way out.
Disabled
A disabled trigger never opens its popover.
Right to left
The popup picks up the direction of the trigger that opened it, even though it renders in a portal. Logical sides like inline-end flip with it.
| Key | Action |
|---|---|
| EnterSpace | On the trigger, opens or closes the popover. Focus moves into the popup. |
| Tab | Moves through the popup’s content. Tabbing out of a non-modal popover closes it. |
| Esc | Closes the popover and returns focus to the trigger. |
<PopoverTitle />and<PopoverDescription />label and describe the popup for screen readers. Include a title whenever the popup contains more than a sentence.- Focus moves to the first focusable element when it opens and back to the trigger when it closes. Change this with
initialFocusandfinalFocus. - With reduced motion enabled, the popup fades without scaling.
Built on the Base UI popover. Every part accepts the props of the primitive it wraps.
| Prop | Type | Default |
|---|---|---|
defaultOpen | boolean | false |
open | boolean | – |
onOpenChangedetails.reason says what caused the change. | (open: boolean, details) => void | – |
onOpenChangeCompleteCalled after the open or close animation ends. | (open: boolean) => void | – |
modaltrue locks page scroll and outside interaction. trap-focus only traps focus. | boolean | "trap-focus" | false |
handleConnects detached triggers. | PopoverHandle<Payload> | – |
children | ReactNode | ({ payload }) => ReactNode | – |
| Prop | Type | Default |
|---|---|---|
openOnHover | boolean | false |
delayMilliseconds before opening on hover. | number | 300 |
closeDelayMilliseconds before closing after hover ends. | number | 0 |
handle | PopoverHandle<Payload> | – |
payloadPassed to the popup when this trigger opens it. | Payload | – |
disabled | boolean | false |
render | ReactElement | (props, state) => ReactElement | <button> |
| Attribute | Description |
|---|---|
data-slot="popover-trigger" | Target the trigger in CSS. |
data-popup-open | Present while its popover is open. |
data-pressed | Present while the trigger is pressed. |
data-disabled | Present when the trigger is disabled. |
Renders the portal, the positioner and the popup in one part.
| Prop | Type | Default |
|---|---|---|
side | "top" | "right" | "bottom" | "left" | "inline-start" | "inline-end" | "bottom" |
align | "start" | "center" | "end" | "center" |
sideOffsetGap between the trigger and the popup. | number | (data) => number | 6 |
alignOffset | number | (data) => number | 0 |
collisionPaddingSpace kept from the edges of the viewport. | number | Rect | 8 |
collisionAvoidanceWhether to flip, shift or neither when space runs out. | CollisionAvoidance | – |
collisionBoundary | Boundary | – |
anchorPosition against something other than the trigger. | Element | RefObject | VirtualElement | () => Element | – |
sticky | boolean | false |
positionMethod | "absolute" | "fixed" | "absolute" |
initialFocusWhere focus goes when the popover opens. | boolean | RefObject | (type) => HTMLElement | boolean | – |
finalFocusWhere focus goes when the popover closes. | boolean | RefObject | (type) => HTMLElement | boolean | – |
portalPropsProps for the portal, such as container. | PortalProps | – |
classNameThe popup is w-72 by default. | string | (state) => string | – |
render | ReactElement | (props, state) => ReactElement | <div> |
| Attribute | Description |
|---|---|
data-slot="popover-content" | The popup. |
data-slot="popover-positioner" | The element that positions the popup. |
data-open | Present while the popover is open. |
data-starting-style | Present while the popup animates in. |
data-ending-style | Present while the popup animates out. |
data-side | The side the popup ended up on. |
data-align | The alignment the popup ended up with. |
data-instant | Present when the change should not animate. |
--transform-origin | The point the popup scales from, at the trigger. |
--available-width | Space between the trigger and the viewport edge. |
--available-height | Space between the trigger and the viewport edge. The popup’s max height. |
--anchor-width | The trigger’s width. |
--anchor-height | The trigger’s height. |
A plain <div> that stacks the title and description.
| Attribute | Description |
|---|---|
data-slot="popover-header" | Target the header in CSS. |
| Prop | Type | Default |
|---|---|---|
render | ReactElement | (props, state) => ReactElement | <h2> |
| Attribute | Description |
|---|---|
data-slot="popover-title" | Labels the popup. |
| Prop | Type | Default |
|---|---|---|
render | ReactElement | (props, state) => ReactElement | <p> |
| Attribute | Description |
|---|---|
data-slot="popover-description" | Describes the popup. |
| Prop | Type | Default |
|---|---|---|
render | ReactElement | (props, state) => ReactElement | <button> |
| Attribute | Description |
|---|---|
data-slot="popover-close" | Closes the popover when pressed. |
createPopoverHandle<Payload>() returns a handle that connects a <Popover /> to triggers rendered elsewhere. Create it once, outside your component.