# Popover

Displays rich content in a portal, triggered by a button.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover"

const styles = stylex.create({
  content: { width: "20rem" },
  label: { flexGrow: 0, flexShrink: 0, flexBasis: "6rem" },
})

const dimensions = [
  { id: "popover-demo-width", label: "Width", value: "100%" },
  { id: "popover-demo-max-width", label: "Max. width", value: "300px" },
  { id: "popover-demo-height", label: "Height", value: "25px" },
  { id: "popover-demo-max-height", label: "Max. height", value: "none" },
]

export default function PopoverDemo() {
  return (
    <Popover>
      <PopoverTrigger render={<Button variant="outline" />}>Open popover</PopoverTrigger>
      <PopoverContent xstyle={styles.content}>
        <PopoverHeader>
          <PopoverTitle>Dimensions</PopoverTitle>
          <PopoverDescription>Set the dimensions for the layer.</PopoverDescription>
        </PopoverHeader>
        <FieldGroup gap={2}>
          {dimensions.map((dimension) => (
            <Field key={dimension.id} orientation="horizontal">
              <FieldLabel xstyle={styles.label}>{dimension.label}</FieldLabel>
              <Input id={dimension.id} defaultValue={dimension.value} />
            </Field>
          ))}
        </FieldGroup>
      </PopoverContent>
    </Popover>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` and `state-styles.ts` from the same page.
   `states.ts` holds the shared opaque surface that the popup box, the dialog and
   the hover card all use. `state-styles.ts` reads the popup's transition state.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/popover.tsx"
   "use client"

   import { useRender } from "@base-ui/react/use-render"
   import type { CSSProperties } from "react"
   import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"

   import { stateStyles } from "@/lib/state-styles"
   import { states } from "@/theme/states"
   import { color, duration, ease, layer, leading, radius, space, text } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     hidden: { opacity: 0 },
     content: {
       zIndex: layer.popup,
       display: "flex",
       flexDirection: "column",
       gap: space.s2_5,
       width: "18rem",
       maxWidth: "var(--available-width)",
       borderRadius: radius.xl,
       padding: space.s2_5,
       fontSize: text.sm,
       lineHeight: leading.sm,
       transformOrigin: "var(--transform-origin)",
       transitionProperty: "opacity, transform",
       transitionDuration: {
         default: duration.fade,
         "@media (prefers-reduced-motion: reduce)": "0s",
       },
       transitionTimingFunction: ease.standard,
     },
     header: { display: "grid", rowGap: space.s0_5 },
     title: { fontWeight: 500 },
     description: { color: color.mutedForeground },
   })

   const slide = stylex.create({
     top: { transform: `scale(0.95) translateY(${space.s2})` },
     bottom: { transform: `scale(0.95) translateY(calc(-1 * ${space.s2}))` },
     left: { transform: `scale(0.95) translateX(${space.s2})` },
     right: { transform: `scale(0.95) translateX(calc(-1 * ${space.s2}))` },
     "inline-start": { transform: `scale(0.95) translateX(${space.s2})` },
     "inline-end": { transform: `scale(0.95) translateX(calc(-1 * ${space.s2}))` },
   })

   type Overrides = { xstyle?: StyleXStyles; className?: string }
   type StateOverrides = Overrides & { style?: CSSProperties }

   function Popover<Payload>(props: PopoverPrimitive.Root.Props<Payload>) {
     return <PopoverPrimitive.Root {...props} />
   }

   function PopoverTrigger<Payload>(props: PopoverPrimitive.Trigger.Props<Payload>) {
     return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />
   }

   function PopoverClose(props: PopoverPrimitive.Close.Props) {
     return <PopoverPrimitive.Close data-slot="popover-close" {...props} />
   }

   type PopoverContentProps = Omit<PopoverPrimitive.Popup.Props, "className" | "style"> &
     Pick<PopoverPrimitive.Positioner.Props, "align" | "alignOffset" | "side" | "sideOffset"> &
     StateOverrides

   function PopoverContent({
     xstyle,
     style,
     className,
     align = "center",
     alignOffset = 0,
     side = "bottom",
     sideOffset = 4,
     ...props
   }: PopoverContentProps) {
     return (
       <PopoverPrimitive.Portal>
         <PopoverPrimitive.Positioner
           align={align}
           alignOffset={alignOffset}
           side={side}
           sideOffset={sideOffset}
           {...stylex.props(states.positioner)}
         >
           <PopoverPrimitive.Popup
             data-slot="popover-content"
             {...props}
             {...stateStyles(
               (state) => [
                 states.popupSurface,
                 styles.content,
                 (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
                   styles.hidden,
                 (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
                   slide[state.side],
                 xstyle,
               ],
               { className, style },
             )}
           />
         </PopoverPrimitive.Positioner>
       </PopoverPrimitive.Portal>
     )
   }

   function PopoverHeader({
     render,
     ref,
     style,
     xstyle,
     className,
     ...props
   }: useRender.ComponentProps<"div"> & Overrides) {
     const sx = stylex.props(styles.header, xstyle)
     return useRender({
       defaultTagName: "div",
       render,
       ref,
       props: {
         "data-slot": "popover-header",
         ...sx,
         ...props,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function PopoverTitle({ xstyle, className, ...props }: PopoverPrimitive.Title.Props & Overrides) {
     const sx = stylex.props(styles.title, xstyle)
     return (
       <PopoverPrimitive.Title
         data-slot="popover-title"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   function PopoverDescription({
     xstyle,
     className,
     ...props
   }: PopoverPrimitive.Description.Props & Overrides) {
     const sx = stylex.props(styles.description, xstyle)
     return (
       <PopoverPrimitive.Description
         data-slot="popover-description"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   export {
     Popover,
     PopoverClose,
     PopoverContent,
     PopoverDescription,
     PopoverHeader,
     PopoverTitle,
     PopoverTrigger,
   }
   ```

4. Update the import paths to match your project setup.

## Usage

```tsx
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover"
```

```tsx
<Popover>
  <PopoverTrigger render={<Button variant="outline" />}>Open</PopoverTrigger>
  <PopoverContent>
    <PopoverHeader>
      <PopoverTitle>Dimensions</PopoverTitle>
      <PopoverDescription>Set the dimensions for the layer.</PopoverDescription>
    </PopoverHeader>
  </PopoverContent>
</Popover>
```

## Composition

```text
Popover
├── PopoverTrigger
└── PopoverContent          portal + positioner + popup
    ├── PopoverHeader
    │   ├── PopoverTitle
    │   └── PopoverDescription
    └── PopoverClose
```

`PopoverTrigger` renders a `<button>`. Pass `render` to use another element as
the trigger, such as a `Button` variant.

The popup is a column with a `0.625rem` gap, so `PopoverHeader` and whatever
follows it need no margins of their own.

## Basic

`PopoverHeader` stacks `PopoverTitle` over `PopoverDescription`. Base UI wires
both to the popup's `aria-labelledby` and `aria-describedby`.

```tsx
import { Button } from "@/components/ui/button"
import {
  Popover,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover"

export default function PopoverBasic() {
  return (
    <Popover>
      <PopoverTrigger render={<Button variant="outline" />}>Open popover</PopoverTrigger>
      <PopoverContent align="start">
        <PopoverHeader>
          <PopoverTitle>Dimensions</PopoverTitle>
          <PopoverDescription>Set the dimensions for the layer.</PopoverDescription>
        </PopoverHeader>
      </PopoverContent>
    </Popover>
  )
}
```

## Align

Use `align` on `PopoverContent` to slide the popup along the trigger's edge, and
`side` to pick which edge it opens from. Base UI flips the side when there is no
room.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { HStack } from "@/components/ui/stack"

const styles = stylex.create({
  content: { width: "10rem" },
})

const alignments = ["start", "center", "end"] as const

export default function PopoverAlignments() {
  return (
    <HStack gap={6}>
      {alignments.map((align) => (
        <Popover key={align}>
          <PopoverTrigger render={<Button variant="outline" size="sm" />}>{align}</PopoverTrigger>
          <PopoverContent align={align} xstyle={styles.content}>
            Aligned to {align}
          </PopoverContent>
        </Popover>
      ))}
    </HStack>
  )
}
```

## With form

Put controls straight in the popup. `PopoverClose` is a button that closes it,
so a cancel and a confirm both go through the same part.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { Field, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import {
  Popover,
  PopoverClose,
  PopoverContent,
  PopoverDescription,
  PopoverHeader,
  PopoverTitle,
  PopoverTrigger,
} from "@/components/ui/popover"
import { HStack } from "@/components/ui/stack"

const styles = stylex.create({
  label: { flexGrow: 0, flexShrink: 0, flexBasis: "4rem" },
  actions: { justifyContent: "flex-end" },
})

export default function PopoverForm() {
  return (
    <Popover>
      <PopoverTrigger render={<Button variant="outline" />}>Open popover</PopoverTrigger>
      <PopoverContent align="start">
        <PopoverHeader>
          <PopoverTitle>Dimensions</PopoverTitle>
          <PopoverDescription>Set the dimensions for the layer.</PopoverDescription>
        </PopoverHeader>
        <FieldGroup gap={2}>
          <Field orientation="horizontal">
            <FieldLabel xstyle={styles.label}>Width</FieldLabel>
            <Input id="popover-form-width" defaultValue="100%" />
          </Field>
          <Field orientation="horizontal">
            <FieldLabel xstyle={styles.label}>Height</FieldLabel>
            <Input id="popover-form-height" defaultValue="25px" />
          </Field>
          <HStack gap={2} xstyle={styles.actions}>
            <PopoverClose render={<Button variant="ghost" size="sm" />}>Cancel</PopoverClose>
            <PopoverClose render={<Button size="sm" />}>Save</PopoverClose>
          </HStack>
        </FieldGroup>
      </PopoverContent>
    </Popover>
  )
}
```

> **Note**
>
> A popover is not modal by default. The page keeps scrolling and a click outside
> closes it. Pass `modal` to `Popover` to lock the page instead, and render a
> `PopoverClose` inside the popup. Base UI only traps focus when it finds one, so
> without it a keyboard user can tab back out of a locked page.

## API reference

Pass `render` to replace a part's element or compose it with another component.

Every part wraps the Base UI `Popover` part of the same name and accepts its
props. See the [Base UI reference](https://base-ui.com/react/components/popover)
for the full list. Each styled part also takes `xstyle` and `className`.

### Popover

| Prop           | Type                               | Default |
| -------------- | ---------------------------------- | ------- |
| `open`         | `boolean`                          | —       |
| `defaultOpen`  | `boolean`                          | `false` |
| `onOpenChange` | `(open: boolean, details) => void` | —       |
| `modal`        | `boolean \| "trap-focus"`          | `false` |
| `handle`       | `PopoverHandle`                    | —       |

### PopoverTrigger

| Prop          | Type      | Default |
| ------------- | --------- | ------- |
| `openOnHover` | `boolean` | `false` |
| `delay`       | `number`  | `300`   |
| `closeDelay`  | `number`  | `0`     |
| `payload`     | `Payload` | —       |

`delay` and `closeDelay` do nothing without `openOnHover`.

### PopoverContent

| Prop          | Type                                     | Default    |
| ------------- | ---------------------------------------- | ---------- |
| `side`        | `"top" \| "bottom" \| "left" \| "right"` | `"bottom"` |
| `sideOffset`  | `number`                                 | `4`        |
| `align`       | `"start" \| "center" \| "end"`           | `"center"` |
| `alignOffset` | `number`                                 | `0`        |

The popup is `18rem` wide and clamped to `--available-width`, so it narrows
rather than overflowing a phone-width viewport. Change the width through
`xstyle`.

### Data attributes

| Attribute                                  | On        | Effect                                           |
| ------------------------------------------ | --------- | ------------------------------------------------ |
| `data-slot="popover-content"`              | the popup | Popup surface                                    |
| `data-side`, `data-align`                  | the popup | Which way it slides in from                      |
| `data-starting-style`, `data-ending-style` | the popup | Base UI writes them while the popup animates     |
| `data-open`, `data-closed`                 | both      | Base UI writes them on the trigger and the popup |
