moduix

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.

Base UI API

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.

PropertyDefaultDescription
--popover-arrow-height0.625remControls the default arrow SVG height.
--popover-arrow-inline-offset0.8125remControls the inline-axis arrow offset.
--popover-arrow-size0.5remControls the block-axis arrow offset.
--popover-arrow-stroke-colorvar(--popover-border-color)Controls arrow border color.
--popover-arrow-width1.25remControls the default arrow SVG width.
--popover-backdrop-bgvar(--backdrop-bg, transparent)Controls backdrop background.
--popover-backdrop-blur0Controls backdrop blur.
--popover-backdrop-transitionvar(--transition-default)Controls backdrop enter and exit transitions.
--popover-bgvar(--color-popover)Controls the popup background color.
--popover-body-margin0Controls body margin.
--popover-border-colorvar(--color-border)Controls the popup border color.
--popover-border-widthvar(--border-width-sm)Controls popup border width.
--popover-colorvar(--color-popover-foreground)Controls the popup text color.
--popover-control-bgvar(--color-background)Controls trigger and close backgrounds.
--popover-control-bg-activevar(--popover-control-bg-hover)Controls trigger background while the popup is open.
--popover-control-bg-hovervar(--color-accent)Controls trigger and close hover backgrounds.
--popover-control-border-colorvar(--color-border)Controls trigger and close border color.
--popover-control-border-widthvar(--border-width-sm)Controls trigger and close border width.
--popover-control-colorvar(--color-foreground)Controls trigger and close text color.
--popover-control-font-sizevar(--text-md)Controls control font size.
--popover-control-heightvar(--size-lg)Controls trigger and close min height.
--popover-control-line-heightvar(--line-height-text-md)Controls control line height.
--popover-control-padding-x0.875remControls control horizontal padding.
--popover-control-padding-y0.5remControls control vertical padding.
--popover-control-radiusvar(--radius-md)Controls trigger and close border radius.
--popover-description-colorvar(--color-muted-foreground)Controls description color.
--popover-description-font-sizevar(--text-sm)Controls description font size.
--popover-description-line-heightvar(--line-height-text-sm)Controls description line height.
--popover-description-margin0Controls description margin.
--popover-disabled-opacityvar(--opacity-disabled)Controls disabled control opacity.
--popover-focus-ring-colorvar(--color-ring)Controls control focus ring color.
--popover-focus-ring-widthvar(--popover-control-border-width)Controls control focus ring width.
--popover-footer-gapvar(--spacing-2)Controls spacing between footer actions.
--popover-footer-justifyflex-endControls footer content alignment.
--popover-footer-marginvar(--spacing-3) 0 0Controls footer margin.
--popover-header-gapvar(--spacing-1)Controls spacing in the header slot.
--popover-heightautoControls the popup height.
--popover-max-height24remControls the popup max height.
--popover-max-width28remControls the popup max width.
--popover-min-width16remControls the popup min width.
--popover-padding-x1remControls the popup horizontal padding.
--popover-padding-y1remControls the popup vertical padding.
--popover-radiusvar(--radius-md)Controls the popup border radius.
--popover-scalevar(--scale-popup)Controls the popup enter and exit scale.
--popover-shadowvar(--shadow-lg)Controls the popup shadow.
--popover-title-colorvar(--popover-color)Controls title color.
--popover-title-font-sizevar(--text-md)Controls title font size.
--popover-title-font-weightvar(--weight-semibold)Controls title font weight.
--popover-title-line-heightvar(--line-height-text-md)Controls title line height.
--popover-transitionvar(--transition-default)Controls popup and control transitions.
--popover-viewport-offset1remControls viewport content transition offset.
--popover-viewport-transition220msControls viewport content transitions.
--popover-widthautoControls the popup width.

Interactive variables scoped for docs preview without changing size scale tokens.

PropertyValueDefaultDescription
--popover-backdrop-bgvar(--backdrop-bg, transparent)Controls backdrop background.
--popover-bgvar(--color-popover)Controls popup background color.
--popover-border-colorvar(--color-border)Controls popup border color.
--popover-colorvar(--color-popover-foreground)Controls popup text color.
--popover-control-bgvar(--color-background)Controls trigger and close backgrounds.
--popover-control-bg-hovervar(--color-accent)Controls control hover backgrounds.
--popover-control-colorvar(--color-foreground)Controls control text color.
--popover-focus-ring-colorvar(--color-ring)Controls focus ring color.
--popover-radiusvar(--radius-md)Controls popup border radius.
--popover-shadowvar(--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>
PartRole
PopoverRoot state machine. Controls open, defaultOpen, onOpenChange, modal, and shared behavior.
PopoverTriggerInteractive anchor that opens and closes the popup. Supports click and optional hover opening.
PopoverContentFloating container that renders popup structure, positioning, and optional arrow/backdrop/viewport plumbing.
PopoverHeaderSemantic top area for title and description. Useful for consistent spacing in information popovers.
PopoverTitleMain heading inside the popup. Helps users quickly identify popup purpose.
PopoverDescriptionSupporting text under the title. Use for concise context or status details.
PopoverBodyOptional free-form content region when header/footer wrappers are not enough.
PopoverFooterOptional action area, commonly for dismiss, confirm, or secondary controls.
PopoverCloseBuilt-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;}

On this page