Popover

A general-purpose floating surface over Radix Popover: focus management, dismiss and collision-aware positioning. Hosts forms, pickers and info cards, with an exposed --surface so nested controls blend in.

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add popover

Usage#

usage.tsxTSX
import {  Popover,  PopoverTrigger,  PopoverContent,} from "@/components/ui/popover"export function Example() {  return (    <Popover>      <PopoverTrigger asChild>        <Button variant="outline">Open</Button>      </PopoverTrigger>      <PopoverContent>Content</PopoverContent>    </Popover>  )}

Anatomy#

import {  Popover,  PopoverTrigger,  PopoverAnchor,  PopoverContent,  PopoverViewport,  PopoverCloseButton,  PopoverMedia,  PopoverTitle,  PopoverDescription,  PopoverClose,  PopoverFooter,} from "@/components/ui/popover"<Popover>  <PopoverTrigger />  <PopoverAnchor />  <PopoverContent>    <PopoverCloseButton />    <PopoverMedia />    <PopoverTitle />    <PopoverDescription />    <PopoverClose />    <PopoverFooter />  </PopoverContent></Popover>// One popover, many triggers<Popover>  <PopoverTrigger payload={a} />  <PopoverTrigger payload={b} />  <PopoverContent>    <PopoverViewport>{(payload) => …}</PopoverViewport>  </PopoverContent></Popover>

With a form#

The canonical use case. Because the content exposes --surface, nested Inputs paint the panel surface rather than the page. Wrap actions in PopoverClose to dismiss on click.

With arrow & close button#

Set showArrow for a pointer toward the trigger, and use PopoverCloseButton for a styled top-right dismiss.

Alignment#

Control placement along the trigger with align (start · center · end) and side / sideOffset, all forwarded to Radix.

Density#

density tunes the content padding: compact for dense application UI. It also cascades from a surrounding DensityProvider.

Open on hover#

Set openOnHover on the trigger (or the root, for every trigger). It opens after delay and stays open while the pointer crosses into the panel and rests in it. A click pins it open, and touch and keyboard keep the click behaviour.

Hovering a link to peek at where it goes is a HoverCard: it never takes focus and never opens on touch, so it only previews. A hover Popover is still a dialog. Its trigger is a button, a click or Enter opens it, and it can hold a form.

Multiple triggers#

Any number of triggers can open one popover. Give each a payload and pass the content a function of it. One panel serves the whole table, and only the active row's trigger reads as expanded.

NameRoleProjects
Design engineer12
Product manager7
Frontend lead19
Support3

Detached triggers#

Triggers can live outside the Popover, in another component. Create a handle once with createPopoverHandle() and pass it to the root and to each trigger. The handle also opens and closes the popover from code.

To control it, pass open and triggerId, and read the new trigger from onOpenChange, which also fires on a switch.

controlled.tsxTSX
const [open, setOpen] = useState(false)const [triggerId, setTriggerId] = useState<string | null>(null)<Popover  open={open}  triggerId={triggerId}  onOpenChange={(next, { triggerId }) => {    setOpen(next)    setTriggerId(triggerId)  }}>  …</Popover>

Animating between triggers#

Moving from one trigger to another never closes the panel. It glides to the new trigger, eases to the new size, and the contents slide in the direction it moved. The first open lands in place. Each payload sets its own width, here w-64 on the card.

A function child renders inside a PopoverViewport. Place one yourself to style the switch. Hide the leaving contents for a plain resize, or key off data-activation-direction, data-current and data-previous.

viewport.tsxTSX
<PopoverContent>  <PopoverViewport className="[&>[data-previous]]:hidden">    {(member: Member) => <MemberCard member={member} />}  </PopoverViewport></PopoverContent>

Confirm an action#

A lightweight inline confirm for a destructive action. The footer pairs a ghost Cancel with a destructive primary, each wrapped in PopoverClose so either choice dismisses.

Profile card#

A richer surface: compose an Avatar with identity and actions for a hover-card style preview.

PopoverMedia frames a picture or video 8px in from the panel's edges, first or last, with corners concentric to the panel's (16px outside, 8px on the picture). PopoverFooter closes the panel with a muted well on the same inset, where a divider line would otherwise go; its text lands back on the copy's line. At compact both sit 4px in, with 12px corners.

A read-only Input blends with the panel through --surface, and the copy button swaps to a confirmation for a beat after a click.

Feedback prompt#

A common composition: pair the surface with a ToggleGroup for a "Was this useful?" rating. The control is its own label, so no inputs are needed. Shown here always visible as a card; the same content drops straight into a PopoverContent.

Was this useful?

How would you rate your overall experience?

A compact take drops the subtitle and swaps in a thank-you once answered.

Was this useful?

And floated in the popover itself, revealed from a trigger.

API reference#

Popover

The root. Owns the open state and the active trigger. Renders no element.

PropTypeDefaultDescription
openboolean-The open state, controlled.
defaultOpenbooleanfalseThe open state on first render, uncontrolled.
onOpenChange(open: boolean, details: PopoverOpenChangeDetails) => void-Fires on open, on close, and on a switch between triggers (still true). details carries reason, triggerId and trigger.
triggerIdstring | null-The active trigger, controlled. Pair with details.triggerId.
defaultTriggerIdstring | nullnullThe active trigger on first render. Without one, the first trigger anchors the panel.
handlePopoverHandle-Links triggers rendered outside this root. From createPopoverHandle().
openOnHoverbooleanfalseOpen every trigger on hover. A trigger's own prop wins.
delaynumber300Milliseconds of hover before it opens.
closeDelaynumber150Milliseconds before it closes once the pointer leaves the trigger or the panel. Enough to cross the gap.
modalbooleanfalseTrap focus in the panel and block the page behind it.

PopoverTrigger

A button that opens the popover. Any number of them can open one popover.

PropTypeDefaultDescription
asChildbooleanfalseRender the child element as the trigger, merging props onto it.
payloadunknown-Handed to the content's render function while this trigger is active.
handlePopoverHandle-Links a trigger outside its Popover.
idstring-Names the trigger for triggerId and handle.open(id). Also its DOM id.
openOnHoverboolean-Open on hover. Inherits the root's.
delaynumber-Hover delay for this trigger. Inherits the root's.
closeDelaynumber-Close delay for this trigger. Inherits the root's.
Data attributeDescription
data-stateopen on the active trigger while the panel is open, else closed.
data-popover-triggerThe trigger's id. aria-expanded and aria-controls follow the active trigger only.

PopoverContent

The floating panel, portalled to the body. Forwards every Radix Popover.Content prop.

PropTypeDefaultDescription
childrenReactNode | (payload) => ReactNode-The contents, or a function of the active payload. A function renders in a PopoverViewport.
side"top" | "right" | "bottom" | "left""bottom"The preferred side of the trigger. Flips on collision.
sideOffsetnumber8The gap to the trigger, in pixels.
align"start" | "center" | "end""center"Alignment along the trigger.
collisionPaddingnumber8The room kept from the viewport edge.
density"comfortable" | "compact""comfortable"The padding: 16px or 12px. Inherits from a DensityProvider.
showArrowbooleanfalseRender a small pointer toward the trigger.
Data attributeDescription
data-stateopen or closed.
data-sideThe side it was placed on, after collisions.
data-alignThe alignment it was placed with.
CSS variableDescription
--surfaceThe panel colour, read by nested controls so they blend with it.
--popover-paddingThe density padding, which the viewport moves onto its contents.
--popover-inset-radiusThe corner of inset media and footer, concentric with the panel.
--radix-popover-content-transform-originThe trigger side the panel grows from.

PopoverViewport

The box the contents switch in. Eases to each payload's size and slides the contents in the direction the panel moved.

PropTypeDefaultDescription
childrenReactNode | (payload) => ReactNode-The contents, or a function of the active payload.
Data attributeDescription
data-activation-directionWhere the new trigger sits from the old: left, right, up or down.
data-instantSet until the first switch, so the first open lands at its size.
data-currentOn the arriving contents.
data-previousOn the leaving contents, until their exit ends.

createPopoverHandle

Creates a handle shared by a root and its detached triggers. Call it once, outside render. Returns open(triggerId?), close() and isOpen.

PopoverAnchor

Anchors the panel to another element, such as a field around the trigger. Replaces the trigger anchor.

PopoverClose

Closes the popover on click. Use asChild to keep your own button.

PopoverCloseButton

A styled close button pinned to the top-right corner. Pass your icon as the child.

PopoverTitle

The panel's heading, an h4.

PopoverDescription

Muted supporting copy under the title.

PopoverMedia

A picture or video set in from the panel's edges, with concentric corners. First or last.

PopoverFooter

A muted well that closes the panel, inset like the media. Always last.

FAQ#

Reach for Popover to host arbitrary content like forms, pickers, or info cards. DropdownMenu is for lists of actions with menu keyboard semantics, and Tooltip is for a passive hover hint.