# Popover

> A React popover for rich content anchored to a trigger, like a quick filter, form or share panel, that becomes a bottom sheet on phones.

- Type: React component (Overlays)
- Access: Free (the source is open to everyone)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add popover`
- Docs: https://wingo-ui.com/components/popover

## Interaction

Click the trigger and the panel grows out of it on a quick spring, pointing at the trigger when it has an arrow. It flips to the other side and slides along the edge when there is no room, and stays anchored while the page scrolls. Focus moves to the first field so you can type at once (on a touch screen the panel itself takes focus, so no keyboard pops up); Escape or a click outside closes it and focus returns to the trigger; Tab past the last control (or Shift+Tab past the first) leaves it in reading order and closes it. A modal popover keeps focus inside and blocks the page until it closes. A shortcut, when set, toggles it from anywhere. Below 768px the same content opens as a bottom sheet you can drag down: the header and footer stay pinned, the body scrolls, and the footer stacks full-width 48px buttons with the primary action on top. With reduced motion it only fades.

## Installation

```bash
npx wingo-ui@latest add popover
```

The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add https://wingo-ui.com/r/popover.json`

Files:

- `components/ui/popover.tsx`

npm dependencies: `npm i @radix-ui/react-popover @radix-ui/react-slot lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { ChevronDown } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Popover, PopoverClose } from "@/components/ui/popover"

export function PriceFilter({ onApply, onReset }: { onApply: () => void; onReset: () => void }) {
  return (
    <Popover
      trigger={
        <Button variant="soft" rightIcon={<ChevronDown />}>
          Price
        </Button>
      }
      title="Price"
      description="Pick a range or type your own."
      align="start"
      arrow
      sheetTitle="Price filter"
      footer={
        <>
          <Button variant="ghost" size="sm" onClick={onReset}>
            Reset
          </Button>
          <PopoverClose asChild>
            <Button size="sm" onClick={onApply}>
              Apply
            </Button>
          </PopoverClose>
        </>
      }
    >
      <p className="text-muted-foreground">Between $500 and $1,500</p>
    </Popover>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen?` | `boolean` | `false` | Starts open (uncontrolled). Pass open and onOpenChange instead to control it. |
| `open?` | `boolean` |  | Controlled open state. The root keeps it across the 768px breakpoint, so turning a tablet never closes the popover. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when it opens or closes: the trigger, Escape, a click outside, focus leaving it, the shortcut or a close button. |
| `title?` | `ReactNode` | `""` | An optional heading at the top, 15px semibold; it names the panel for screen readers. The demo shows its sample while this is empty. |
| `description?` | `ReactNode` | `""` | A quieter line under the title, wired to aria-describedby. |
| `closeLabel?` | `string` | `"Close"` | aria-label of the close button, e.g. a translated “Close” for another language. |
| `sheetTitle?` | `string` | `"Details"` | The name screen readers hear while there is no title: the bottom sheet's hidden title on phones and the panel's aria-label on desktop. |
| `trigger?` | `ReactNode` |  | The element that opens it, rendered with asChild: a single element that takes a ref, like a Button. It gets aria-expanded, aria-controls, data-state and, with a shortcut, aria-keyshortcuts. |
| `children?` | `ReactNode` |  | The body, 14px text. It scrolls when the panel would be taller than the space on screen. |
| `footer?` | `ReactNode` |  | Actions under the body, primary last: a right-aligned row of size="sm" buttons in the panel, stacked full width with the primary on top in the sheet (buttons grow to 48px there on their own). |
| `side?` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` | The side of the trigger it opens on. It flips to the opposite side when there is no room. |
| `align?` | `"start" \| "center" \| "end"` | `"center"` | How it lines up with the trigger along that side: its start edge, its center or its end edge. |
| `sideOffset?` | `number` | `8` | Gap in px between the trigger and the panel (the arrow adds its own 8px). |
| `alignOffset?` | `number` | `0` | Shifts the panel along the trigger's edge, in px, from where align puts it. |
| `size?` | `"sm" \| "md" \| "lg" \| "auto"` | `"md"` | Width 256 / 288 / 384px, or auto to fit the content (at least 192px). Always at most the screen width minus 24px. |
| `padding?` | `"none" \| "sm" \| "md" \| "lg"` | `"md"` | Space inside the panel: 0 / 12 / 16 / 20px. none is for full-bleed lists and media. The sheet on phones keeps its own 20px sides (check usePopover().sheet to lay out rows for it). |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner radius 0 / 12 / 16 / 24px (full is 24px, like every floating surface). The sheet keeps its 28px top corners. |
| `arrow?` | `boolean` | `false` | A 16 x 8 arrow with a rounded tip pointing at the trigger, in the panel's color with the same hairline. It keeps clear of the rounded corners and hides when it cannot point at the trigger. |
| `showClose?` | `boolean` | `false` | A round X in the top corner, centered on the title's line. Escape and a click outside always close it too. |
| `modal?` | `boolean` | `false` | Traps focus inside, blocks clicks on the page and locks its scroll until the popover closes. For a confirm or a form that must be finished or cancelled. |
| `responsive?` | `boolean` | `true` | Below 768px it opens as the drawer's bottom sheet: handle, drag to dismiss, pinned header and footer, safe-area padding. false keeps the floating panel on phones (an autocomplete under a field). |
| `shortcut?` | `string` | `""` | A key combo in mod+k notation (⌘ on Apple devices, Ctrl elsewhere) that toggles it from anywhere on the page; the trigger gets aria-keyshortcuts. Empty binds nothing. |
| `onOpenAutoFocus?` | `(event: Event) => void` |  | Runs when it opens, before focus moves in. Call event.preventDefault() to keep focus where it is (an anchored suggestion list under a field). |
| `container?` | `HTMLElement \| null` |  | Portal target, document.body by default. The sheet renders there too. |
| `classNames?` | `Partial<Record<"content" \| "viewport" \| "header" \| "title" \| "description" \| "body" \| "footer" \| "close" \| "arrow", string>>` |  | Extra classes for the inner parts: the panel, its scrolling viewport (which holds the padding), the header, title, description, body, footer, the close button and the arrow. The same keys style the sheet's parts on phones. |
| `className?` | `string` |  | Extra classes for the panel (popover-content), merged last, e.g. w-(--radix-popover-trigger-width) to match an anchor. A width here never narrows the sheet. |

## Parts

### PopoverRoot (component)

Compound API: holds the state, binds the shortcut and picks the mode (a Radix popover, or the drawer below 768px). No DOM. Props: { open, defaultOpen, onOpenChange, modal, responsive, shortcut, container }.

### PopoverTrigger (component)

Opens and closes it; gets aria-expanded, aria-controls and aria-keyshortcuts when a shortcut is set. In the sheet it is the drawer trigger. Props: Radix Popover.Trigger props (asChild).

### PopoverAnchor (component)

Positions the panel against another element, such as a field, while the trigger stays elsewhere (or is left out); in the sheet it is a plain wrapper. Props: Radix Popover.Anchor props (asChild).

### PopoverContent (component)

The portal and the panel (popover-content with data-side, data-align, data-size) around a scrolling viewport, plus the close button and the arrow. In the sheet, loose children become the body on their own; keep the header, body and footer as direct children (fragments are fine) so the sheet can pin them. Props: { side, align, sideOffset, alignOffset, size, radius, padding, arrow, showClose, closeLabel, sheetTitle, container, classNames } & Radix Popover.Content props.

### PopoverHeader (component)

The heading block (title and description stacked, gap 4px). Renders nothing when empty; in the sheet it is the drawer header. Props: { title?, description? } & div props.

### PopoverTitle (component)

15px semibold title that labels the panel (aria-labelledby). In the sheet it is the drawer title. Props: h2 props.

### PopoverDescription (component)

13px muted line that describes the panel (aria-describedby). In the sheet it is the drawer description. Props: p props.

### PopoverBody (component)

The body, 14px text; the scroll region in the sheet. Takes div props.

### PopoverFooter (component)

The actions row, primary last on the right; stacked full width with the primary on top in the sheet. Takes div props.

### PopoverClose (component)

Closes it from any element, such as an Apply button. Props: Radix Popover.Close props (asChild).

### PopoverArrow (component)

The 16 x 8 arrow that arrow={true} renders, in the popover color; nothing in the sheet. Props: Radix Popover.Arrow props.

### usePopover (hook)

The nearest popover's state, e.g. to close it after a save or to size footer buttons for the sheet. Signature: () => { open, setOpen, sheet }.

## Examples

- **Sides, aligns and the arrow**: Each side with arrow; the pills set align for all four. It flips when there is no room. responsive={false} keeps it floating on phones.
- **Share panel**: A read-only link with a copy button in brand soft, and share targets. showClose puts a round × in the corner.
- **Quick edit**: The parts API with a controlled open state: focus lands in the first field, Enter saves, the save runs 1.4s and closes it.
- **Sizes**: sm, md and lg are 256, 288 and 384px wide; auto fits the content.
- **Anchored to a field**: PopoverAnchor pins the list under the input and w-(--radix-popover-trigger-width) matches its width. Focus stays in the field; ↓ walks the dates.
- **Modal confirm**: modal traps focus and blocks the page until you answer. The row slides out after Delete.
- **Notifications**: padding="none" and size="lg" for full-bleed rows, a header action, alt+n to toggle. On a phone the same parts become a sheet.

## See also

- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Sheet](https://wingo-ui.com/components/sheet) ([Markdown](https://wingo-ui.com/components/sheet.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [Hover Card](https://wingo-ui.com/components/hover-card) ([Markdown](https://wingo-ui.com/components/hover-card.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))

---

Canonical: https://wingo-ui.com/components/popover
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
