Popover
An accessible popup anchored to a trigger for compact contextual content.
API Reference
Original primitive API
Behavior, accessibility details, and low-level props are documented by Base UI.
Basic
import { BellIcon, Button, Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger,} from "moduix";export function PopoverDemo() { return ( <Popover> <PopoverTrigger render={<Button />}> <span className={styles.triggerContent}> <BellIcon className={styles.icon} /> Notifications </span> </PopoverTrigger> <PopoverContent> <PopoverHeader> <PopoverTitle>Notifications</PopoverTitle> <PopoverDescription>You are all caught up. Good job!</PopoverDescription> </PopoverHeader> </PopoverContent> </Popover> );}.triggerContent { display: inline-flex; align-items: center; gap: var(--spacing-2);}.icon { width: 1rem; height: 1rem;}Full list of component variables available for project-level overrides.
| Property | Default | Description |
|---|---|---|
| --popover-arrow-height | 0.625rem | Controls the default arrow SVG height. |
| --popover-arrow-inline-offset | 0.8125rem | Controls the inline-axis arrow offset. |
| --popover-arrow-size | 0.5rem | Controls the block-axis arrow offset. |
| --popover-arrow-stroke-color | var(--popover-border-color) | Controls arrow border color. |
| --popover-arrow-width | 1.25rem | Controls the default arrow SVG width. |
| --popover-backdrop-bg | var(--backdrop-bg, transparent) | Controls backdrop background. |
| --popover-backdrop-blur | 0 | Controls backdrop blur. |
| --popover-backdrop-transition | var(--transition-default) | Controls backdrop enter and exit transitions. |
| --popover-bg | var(--color-popover) | Controls the popup background color. |
| --popover-body-margin | 0 | Controls body margin. |
| --popover-border-color | var(--color-border) | Controls the popup border color. |
| --popover-border-width | var(--border-width-sm) | Controls popup border width. |
| --popover-color | var(--color-popover-foreground) | Controls the popup text color. |
| --popover-control-bg | var(--color-background) | Controls trigger and close backgrounds. |
| --popover-control-bg-active | var(--popover-control-bg-hover) | Controls trigger background while the popup is open. |
| --popover-control-bg-hover | var(--color-accent) | Controls trigger and close hover backgrounds. |
| --popover-control-border-color | var(--color-border) | Controls trigger and close border color. |
| --popover-control-border-width | var(--border-width-sm) | Controls trigger and close border width. |
| --popover-control-color | var(--color-foreground) | Controls trigger and close text color. |
| --popover-control-font-size | var(--text-md) | Controls control font size. |
| --popover-control-height | var(--size-lg) | Controls trigger and close min height. |
| --popover-control-line-height | var(--line-height-text-md) | Controls control line height. |
| --popover-control-padding-x | 0.875rem | Controls control horizontal padding. |
| --popover-control-padding-y | 0.5rem | Controls control vertical padding. |
| --popover-control-radius | var(--radius-md) | Controls trigger and close border radius. |
| --popover-description-color | var(--color-muted-foreground) | Controls description color. |
| --popover-description-font-size | var(--text-sm) | Controls description font size. |
| --popover-description-line-height | var(--line-height-text-sm) | Controls description line height. |
| --popover-description-margin | 0 | Controls description margin. |
| --popover-disabled-opacity | var(--opacity-disabled) | Controls disabled control opacity. |
| --popover-focus-ring-color | var(--color-ring) | Controls control focus ring color. |
| --popover-focus-ring-width | var(--popover-control-border-width) | Controls control focus ring width. |
| --popover-footer-gap | var(--spacing-2) | Controls spacing between footer actions. |
| --popover-footer-justify | flex-end | Controls footer content alignment. |
| --popover-footer-margin | var(--spacing-3) 0 0 | Controls footer margin. |
| --popover-header-gap | var(--spacing-1) | Controls spacing in the header slot. |
| --popover-height | auto | Controls the popup height. |
| --popover-max-height | 24rem | Controls the popup max height. |
| --popover-max-width | 28rem | Controls the popup max width. |
| --popover-min-width | 16rem | Controls the popup min width. |
| --popover-padding-x | 1rem | Controls the popup horizontal padding. |
| --popover-padding-y | 1rem | Controls the popup vertical padding. |
| --popover-radius | var(--radius-md) | Controls the popup border radius. |
| --popover-scale | var(--scale-popup) | Controls the popup enter and exit scale. |
| --popover-shadow | var(--shadow-lg) | Controls the popup shadow. |
| --popover-title-color | var(--popover-color) | Controls title color. |
| --popover-title-font-size | var(--text-md) | Controls title font size. |
| --popover-title-font-weight | var(--weight-semibold) | Controls title font weight. |
| --popover-title-line-height | var(--line-height-text-md) | Controls title line height. |
| --popover-transition | var(--transition-default) | Controls popup and control transitions. |
| --popover-viewport-offset | 1rem | Controls viewport content transition offset. |
| --popover-viewport-transition | 220ms | Controls viewport content transitions. |
| --popover-width | auto | Controls the popup width. |
Interactive variables scoped for docs preview without changing size scale tokens.
| Property | Value | Default | Description |
|---|---|---|---|
| --popover-backdrop-bg | var(--backdrop-bg, transparent) | Controls backdrop background. | |
| --popover-bg | var(--color-popover) | Controls popup background color. | |
| --popover-border-color | var(--color-border) | Controls popup border color. | |
| --popover-color | var(--color-popover-foreground) | Controls popup text color. | |
| --popover-control-bg | var(--color-background) | Controls trigger and close backgrounds. | |
| --popover-control-bg-hover | var(--color-accent) | Controls control hover backgrounds. | |
| --popover-control-color | var(--color-foreground) | Controls control text color. | |
| --popover-focus-ring-color | var(--color-ring) | Controls focus ring color. | |
| --popover-radius | var(--radius-md) | Controls popup border radius. | |
| --popover-shadow | var(--shadow-lg) | Controls popup shadow. |
Anatomy
Popover combines a trigger and a floating content layer. Keep PopoverTrigger and
PopoverContent under the same Popover root unless you intentionally connect them with
createPopoverHandle.
Popover
├─ PopoverTrigger
│ └─ trigger content
└─ PopoverContent
├─ PopoverHeader (optional)
│ ├─ PopoverTitle (optional)
│ └─ PopoverDescription (optional)
├─ PopoverBody (optional)
├─ PopoverFooter (optional)
│ └─ PopoverClose (optional)
└─ arrow (optional)
PopoverContent service slots (optional)
├─ portal
├─ backdrop
├─ positioner
└─ viewport<Popover>
<PopoverTrigger render={<Button />}>Open popover</PopoverTrigger>
<PopoverContent withBackdrop withViewport>
<PopoverHeader>
<PopoverTitle>Popover title</PopoverTitle>
<PopoverDescription>Short contextual details.</PopoverDescription>
</PopoverHeader>
<PopoverBody>Popup content</PopoverBody>
<PopoverFooter>
<PopoverClose>Close</PopoverClose>
</PopoverFooter>
</PopoverContent>
</Popover>| Part | Role |
|---|---|
Popover | Root state machine. Controls open, defaultOpen, onOpenChange, modal, and shared behavior. |
PopoverTrigger | Interactive anchor that opens and closes the popup. Supports click and optional hover opening. |
PopoverContent | Floating container that renders popup structure, positioning, and optional arrow/backdrop/viewport plumbing. |
PopoverHeader | Semantic top area for title and description. Useful for consistent spacing in information popovers. |
PopoverTitle | Main heading inside the popup. Helps users quickly identify popup purpose. |
PopoverDescription | Supporting text under the title. Use for concise context or status details. |
PopoverBody | Optional free-form content region when header/footer wrappers are not enough. |
PopoverFooter | Optional action area, commonly for dismiss, confirm, or secondary controls. |
PopoverClose | Built-in close action that dismisses the current popover without custom state wiring. |
In most cases, style the visible popup layers first: PopoverTrigger, PopoverContent, and
header/body/footer content. Use classNames for internal service slots (portal, backdrop,
positioner, viewport, arrow) only when you need advanced placement, overlays, or transition
coordination.
Composition
Use Popover for root state and behavior props such as open, defaultOpen,
onOpenChange, modal, and handle. Set withBackdrop when the popup needs an overlay and
withViewport when one popover switches between trigger payloads with animated content.
className styles the visible popup. classNames styles internal service slots that are
hidden from the default composition. Pass placement props such as side, align, sideOffset,
alignOffset, collisionBoundary, and collisionPadding directly to PopoverContent. Use
container to choose the portal container, withBackdrop to enable the optional backdrop,
withViewport to wrap content in Base UI's viewport, withArrow={false} to remove the default
arrow, and arrow to replace it. Use slotProps only when you need Base UI escape-hatch props for
an internal slot, such as keepMounted on the portal or a custom render for the viewport:
<PopoverContent
withBackdrop
withViewport
className={styles.popup}
classNames={{
portal: styles.portal,
backdrop: styles.backdrop,
positioner: styles.positioner,
viewport: styles.viewport,
arrow: styles.arrow,
}}
slotProps={{
portal: { keepMounted: true },
positioner: { collisionPadding: 12 },
}}
/>Examples
With Close Action
Use PopoverClose inside the popup when the content includes an explicit dismiss action.
import { Button, Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverFooter, PopoverHeader, PopoverTitle, PopoverTrigger,} from "moduix";export function PopoverWithCloseActionDemo() { return ( <Popover> <PopoverTrigger render={<Button />}>Project status</PopoverTrigger> <PopoverContent> <PopoverHeader> <PopoverTitle>Sprint 19</PopoverTitle> <PopoverDescription> 9 tasks completed, 2 in progress. Everything is on schedule. </PopoverDescription> </PopoverHeader> <PopoverFooter> <PopoverClose>Close</PopoverClose> </PopoverFooter> </PopoverContent> </Popover> );}With Backdrop
Set withBackdrop on PopoverContent when the popup should visually separate itself from the page.
Use classNames to style internal service slots without rendering them manually.
import { Button, Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger,} from "moduix";export function PopoverWithBackdropDemo() { return ( <Popover> <PopoverTrigger className={styles.backdropTrigger} render={<Button />}> Open with backdrop </PopoverTrigger> <PopoverContent withArrow={false} withBackdrop classNames={{ backdrop: styles.backdrop }} > <PopoverHeader> <PopoverTitle>Backdrop</PopoverTitle> <PopoverDescription> The backdrop is rendered automatically and styled through classNames. </PopoverDescription> </PopoverHeader> </PopoverContent> </Popover> );}.backdrop { background-color: color-mix(in oklab, var(--color-background), transparent 35%); backdrop-filter: blur(2px);}.backdropTrigger { position: relative; z-index: calc(var(--z-popup) + 1);}Open On Hover
Set openOnHover on the trigger for quick preview interactions. Use delay and closeDelay to
tune hover timing.
import { Button, Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger,} from "moduix";export function OpenOnHoverPopoverDemo() { return ( <Popover> <PopoverTrigger openOnHover delay={150} closeDelay={120} render={<Button />}> Open on hover </PopoverTrigger> <PopoverContent> <PopoverHeader> <PopoverTitle>Hover mode</PopoverTitle> <PopoverDescription> This popover uses delayed hover opening for quick preview interactions. </PopoverDescription> </PopoverHeader> </PopoverContent> </Popover> );}Controlled
Control the open state from React when the popover needs to coordinate with other UI.
import { Button, Popover, PopoverClose, PopoverContent, PopoverDescription, PopoverFooter, PopoverHeader, PopoverTitle, PopoverTrigger,} from "moduix";import { useState } from "react";export function ControlledPopoverDemo() { const [open, setOpen] = useState(false); return ( <Popover open={open} onOpenChange={setOpen}> <PopoverTrigger render={<Button />}>Open controlled popover</PopoverTrigger> <PopoverContent> <PopoverHeader> <PopoverTitle>Publish changes?</PopoverTitle> <PopoverDescription> This action will make your latest updates visible to all users. </PopoverDescription> </PopoverHeader> <PopoverFooter> <PopoverClose>Back to editing</PopoverClose> </PopoverFooter> </PopoverContent> </Popover> );}Detached Trigger
Use createPopoverHandle when the trigger lives outside the popover tree.
import { Button, Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, createPopoverHandle,} from "moduix";import { useMemo } from "react";export function DetachedTriggerPopoverDemo() { const popoverHandle = useMemo(() => createPopoverHandle(), []); return ( <div> <PopoverTrigger handle={popoverHandle} render={<Button />}> Open details </PopoverTrigger> <Popover handle={popoverHandle}> <PopoverContent> <PopoverHeader> <PopoverTitle>Detached trigger</PopoverTitle> <PopoverDescription> Trigger and popup are linked with createPopoverHandle(). </PopoverDescription> </PopoverHeader> </PopoverContent> </Popover> </div> );}Side Control
Pass side, align, offsets, collision options, or slotProps.positioner to PopoverContent
when you need to control popup placement.
import { Button, Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger,} from "moduix";import { useState } from "react";export function SideControlPopoverDemo() { const [side, setSide] = useState("bottom" as (typeof sides)[number]); return ( <div className={styles.stack}> <div className={styles.sideButtons}> {sides.map((item) => ( <button key={item} type="button" className={styles.sideButton} data-active={item === side || undefined} onClick={() => setSide(item)} > {item} </button> ))} </div> <Popover> <PopoverTrigger render={<Button />}>Open with side: {side}</PopoverTrigger> <PopoverContent side={side} className={styles.narrowPopup}> <PopoverHeader> <PopoverTitle>Placement</PopoverTitle> <PopoverDescription> Current side is <strong>{side}</strong>. </PopoverDescription> </PopoverHeader> </PopoverContent> </Popover> </div> );}.stack { display: flex; flex-direction: column; align-items: center; gap: var(--spacing-3);}.sideButtons { display: inline-flex; align-items: center; flex-wrap: wrap; gap: var(--spacing-1); padding: var(--spacing-1); border: var(--border-width-sm) solid var(--color-border); border-radius: var(--radius-md); background: var(--color-muted);}.sideButton { min-width: 4.75rem; min-height: 2rem; padding: 0 var(--spacing-3); border: var(--border-width-sm) solid var(--color-border); border-radius: var(--radius-sm); background: var(--color-background); color: var(--color-foreground); font: inherit; font-size: var(--text-sm); line-height: var(--line-height-text-sm); font-weight: var(--weight-medium); text-transform: capitalize; cursor: pointer; transition: background-color var(--transition-default), color var(--transition-default); &[data-active] { background: var(--color-primary); color: var(--color-primary-foreground); }}.narrowPopup { --popover-max-width: 20rem;}const sides = ["top", "right", "bottom", "left"] as const;Image Only Content
Use PopoverBody and popup-level CSS variables when the content does not need title and
description slots.
import { Button, Popover, PopoverBody, PopoverContent, PopoverTrigger,} from "moduix";export function ImageOnlyPopoverDemo() { return ( <Popover> <PopoverTrigger render={<Button />}>Open image popover</PopoverTrigger> <PopoverContent className={styles.imagePopup}> <PopoverBody> <img className={styles.image} alt="Abstract geometric composition" src={imageUrl} /> </PopoverBody> </PopoverContent> </Popover> );}.imagePopup { --popover-padding-x: var(--spacing-2); --popover-padding-y: var(--spacing-2);}.image { display: block; width: 18rem; max-width: min(18rem, calc(100vw - 4rem)); height: 10.5rem; border-radius: var(--radius-md); object-fit: cover;}const imageUrl = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 720 420'%3E%3Cdefs%3E%3ClinearGradient id='bg' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop stop-color='%230b1220' offset='0'/%3E%3Cstop stop-color='%231d3557' offset='0.52'/%3E%3Cstop stop-color='%23004e64' offset='1'/%3E%3C/linearGradient%3E%3ClinearGradient id='accent1' x1='0' y1='0' x2='1' y2='0'%3E%3Cstop stop-color='%23ffd166'/%3E%3Cstop stop-color='%23fca311'/%3E%3C/linearGradient%3E%3ClinearGradient id='accent2' x1='0' y1='1' x2='1' y2='0'%3E%3Cstop stop-color='%2306d6a0'/%3E%3Cstop stop-color='%23118ab2'/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect width='720' height='420' fill='url(%23bg)'/%3E%3Ccircle cx='120' cy='90' r='70' fill='%23ffffff22'/%3E%3Ccircle cx='620' cy='330' r='120' fill='%23ffffff18'/%3E%3Crect x='70' y='240' width='320' height='110' rx='22' fill='url(%23accent1)' opacity='0.88' transform='rotate(-8 230 295)'/%3E%3Crect x='320' y='90' width='300' height='120' rx='24' fill='url(%23accent2)' opacity='0.92' transform='rotate(10 470 150)'/%3E%3Cpath d='M40 370 C160 260, 270 360, 390 280 C510 200, 610 280, 720 210 L720 420 L40 420 Z' fill='%23ffffff22'/%3E%3C/svg%3E";Without Arrow
Set withArrow={false} when the popup should look like a floating panel without a pointer.
import { Button, Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger,} from "moduix";export function PopoverWithoutArrowDemo() { return ( <Popover> <PopoverTrigger render={<Button />}>Open without arrow</PopoverTrigger> <PopoverContent withArrow={false}> <PopoverHeader> <PopoverTitle>No arrow</PopoverTitle> <PopoverDescription> Set arrow to false when the popup should look like a floating panel. </PopoverDescription> </PopoverHeader> </PopoverContent> </Popover> );}Custom Styles
Use className for the popup and classNames for the automatically rendered portal, backdrop,
positioner, viewport, and arrow slots.
import { Button, CheckSmallIcon, Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger,} from "moduix";export function CustomStylesPopoverDemo() { return ( <Popover> <PopoverTrigger className={styles.customTrigger} render={<Button />}> Open custom styles </PopoverTrigger> <PopoverContent withBackdrop withViewport className={styles.customPopup} classNames={{ portal: styles.customPortal, backdrop: styles.customBackdrop, positioner: styles.customPositioner, viewport: styles.customViewport, arrow: styles.customArrowSlot, }} arrow={<CheckSmallIcon className={styles.customArrowIcon} />} > <PopoverHeader> <PopoverTitle>Custom styles</PopoverTitle> <PopoverDescription> Popup, portal, backdrop, positioner, viewport, and arrow slots are styled through className and classNames. </PopoverDescription> </PopoverHeader> </PopoverContent> </Popover> );}.customPortal { pointer-events: auto;}.customBackdrop { background-color: rgb(15 23 42 / 0.42); backdrop-filter: blur(3px);}.customPositioner { transition: top 220ms, left 220ms, right 220ms, bottom 220ms, transform 220ms;}.customViewport { --popover-viewport-offset: var(--spacing-3); --popover-viewport-transition: 260ms;}.customTrigger { position: relative; z-index: calc(var(--z-popup) + 1);}.customPopup { --popover-bg: var(--color-primary); --popover-color: var(--color-primary-foreground); --popover-border-color: color-mix(in oklab, var(--color-primary), black 18%); --popover-arrow-stroke-color: var(--popover-border-color); --popover-title-color: var(--popover-color); --popover-description-color: color-mix(in oklab, var(--popover-color), transparent 18%); --popover-radius: var(--radius-lg); --popover-shadow: var(--shadow-xl);}.customArrowSlot { width: 1.625rem; height: 1.625rem; align-items: center; justify-content: center; border: var(--border-width-sm) solid var(--popover-border-color); border-radius: 999px; background: var(--popover-bg); color: var(--popover-color);}.customArrowIcon { width: 1rem; height: 1rem;}