# Popover (/docs/popover)





## API Reference [#api-reference]

<BaseUIReference href="https://base-ui.com/react/components/popover" />

## Basic [#basic]

<Preview cssProperties="popoverPlaygroundCssProperties">
  <PopoverExample />

  <Preview.Code>
    {`
          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>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .triggerContent {
            display: inline-flex;
            align-items: center;
            gap: var(--spacing-2);
          }

          .icon {
            width: 1rem;
            height: 1rem;
          }
        `}
  </Preview.CSS>

  <Preview.CSSProperties>
    {(context) => <PopoverCssPropertiesPanel {...context} />}
  </Preview.CSSProperties>

  <Preview.CSSPlayground>
    {(context) => <PopoverCssPlaygroundPanel {...context} />}
  </Preview.CSSPlayground>
</Preview>

## Anatomy [#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`.

```text
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
```

```tsx
<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 [#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:

```tsx
<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 [#examples]

### With Close Action [#with-close-action]

Use `PopoverClose` inside the popup when the content includes an explicit dismiss action.

<Preview>
  <PopoverWithCloseActionExample />

  <Preview.Code>
    {`
          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>
            );
          }
        `}
  </Preview.Code>
</Preview>

### With Backdrop [#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.

<Preview>
  <PopoverWithBackdropExample />

  <Preview.Code>
    {`
          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>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .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);
          }
        `}
  </Preview.CSS>
</Preview>

### Open On Hover [#open-on-hover]

Set `openOnHover` on the trigger for quick preview interactions. Use `delay` and `closeDelay` to
tune hover timing.

<Preview>
  <OpenOnHoverPopoverExample />

  <Preview.Code>
    {`
          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>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Controlled [#controlled]

Control the open state from React when the popover needs to coordinate with other UI.

<Preview>
  <ControlledPopoverExample />

  <Preview.Code>
    {`
          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>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Detached Trigger [#detached-trigger]

Use `createPopoverHandle` when the trigger lives outside the popover tree.

<Preview>
  <DetachedTriggerPopoverExample />

  <Preview.Code>
    {`
          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>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Side Control [#side-control]

Pass `side`, `align`, offsets, collision options, or `slotProps.positioner` to `PopoverContent`
when you need to control popup placement.

<Preview>
  <SideControlPopoverExample />

  <Preview.Code>
    {`
          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>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .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;
          }
        `}
  </Preview.CSS>

  <Preview.Data>
    {`
          const sides = ["top", "right", "bottom", "left"] as const;
        `}
  </Preview.Data>
</Preview>

### Image Only Content [#image-only-content]

Use `PopoverBody` and popup-level CSS variables when the content does not need title and
description slots.

<Preview>
  <ImageOnlyPopoverExample />

  <Preview.Code>
    {`
          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>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .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;
          }
        `}
  </Preview.CSS>

  <Preview.Data>
    {`
          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";
        `}
  </Preview.Data>
</Preview>

### Without Arrow [#without-arrow]

Set `withArrow={false}` when the popup should look like a floating panel without a pointer.

<Preview>
  <PopoverWithoutArrowExample />

  <Preview.Code>
    {`
          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>
            );
          }
        `}
  </Preview.Code>
</Preview>

### Custom Styles [#custom-styles]

Use `className` for the popup and `classNames` for the automatically rendered portal, backdrop,
positioner, viewport, and arrow slots.

<Preview>
  <CustomStylesPopoverExample />

  <Preview.Code>
    {`
          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>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .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;
          }
        `}
  </Preview.CSS>
</Preview>
