# Shadcn Popover

> Free shadcn/ui popover for React — a Radix floating panel anchored to a trigger. Positioning (side/align/offset), forms that close on submit, settings, notifications, and profile panels. The examples official shadcn never shipped.

Source: https://designrevision.com/components/popover

The **Shadcn Popover** is the floating panel — the canonical <a href="https://ui.shadcn.com/docs/components/popover" rel="nofollow">shadcn/ui</a> popover built on <a href="https://www.radix-ui.com/primitives/docs/components/popover" rel="nofollow">Radix</a>, a generic content panel anchored to a trigger. Official ships one form demo; the examples below show the real range — **forms, settings, notifications, profile cards, and precise positioning**.

## Popover vs Dropdown Menu vs Tooltip vs Dialog

A popover overlaps with three other components, so pick deliberately:

- **Popover** — a panel of **arbitrary content** (fields, toggles, a card) anchored to a trigger. Opens on click, closes on outside-click and Escape.
- **[Dropdown Menu](/components/dropdown-menu)** — a **menu of actions** with roving-focus keyboard semantics. Use it when every row is a command.
- **[Tooltip](/components/tooltip)** — a **hover hint**, non-interactive, for a short label.
- **[Dialog](/components/dialog)** — a **centered modal** that interrupts the page. Use it when the task deserves full attention rather than a small anchored panel.

If the panel holds inputs or rich content, it's a Popover; if it's a list of commands, it's a Dropdown Menu.

## Positioning

Position `PopoverContent` with `side` (`"top" | "right" | "bottom" | "left"`, default `"bottom"`), `align` (`"start" | "center" | "end"`, default `"center"`), and `sideOffset` for the gap. Radix automatically **flips and shifts** the panel to stay in the viewport, so you set a preference, not a fixed position. The Positioning example opens from all four sides.

## Forms in a popover

A popover is the lightweight alternative to a [Dialog](/components/dialog) for a quick edit — rename a project, set dimensions, tweak a value. Control the `open` state and call `setOpen(false)` on submit so the panel closes when the work is done. The Controlled form example is the full pattern; the dimensions form is the canonical shadcn demo.

## Content panels

Because the content is arbitrary, a popover is the home for small **panels**:

- **Settings** — labelled [Switch](/components/switch) rows behind a gear icon (the Settings panel example).
- **Notifications** — a bell trigger with an unread badge over a scrollable list (the Notifications panel example).
- **Profile** — a @mention or avatar opening a card with a follow button (the Profile card example).
- **Help** — a "?" beside a field for inline contextual help (the Info / help example).

## Anchor & dismissal

By default the popover anchors to its trigger; wrap a different element in `PopoverAnchor` to anchor it elsewhere. It closes on outside-click and Escape and returns focus to the trigger; to keep it open during a multi-step flow, `preventDefault()` in `onInteractOutside` / `onEscapeKeyDown`.

## Accessibility & theming

Radix manages focus (it moves into the panel on open and returns to the trigger on close), wires `aria-expanded` / `aria-controls`, and handles Escape — so keyboard and screen-reader users get a coherent experience. The popover reads your design tokens — `--popover`, `--popover-foreground`, `--border` — so it follows your theme, including dark mode, with no overrides.

## Installation

### popover

**Install:**

```bash
npx shadcn@latest add @designrevision/popover
```

**Dependencies:** @radix-ui/react-popover, utils

**Props**

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| side | "top" \| "right" \| "bottom" \| "left" | "bottom" | Which side of the trigger the content opens on (on PopoverContent). |
| align | "start" \| "center" \| "end" | "center" | Alignment against the trigger (on PopoverContent). |
| sideOffset | number | 4 | Distance in px between the trigger and the content (on PopoverContent). |
| open | boolean | — | Controlled open state (on Popover). Pair with onOpenChange. |
| onOpenChange | (open: boolean) => void | — | Fires when the popover opens or closes (on Popover). |

**Usage & accessibility:** A Radix popover — four parts (Popover, PopoverTrigger, PopoverContent, PopoverAnchor). A generic floating panel anchored to a trigger; position it with side / align / sideOffset on PopoverContent. Closes on outside-click and Escape. For a menu of actions use Dropdown Menu; for hover hints use Tooltip; for a searchable list use Command (Combobox).

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

import * as React from "react"
import * as PopoverPrimitive from "@radix-ui/react-popover"

import { cn } from "@/lib/utils"

function Popover({
  ...props
}: React.ComponentProps<typeof PopoverPrimitive.Root>) {
  return <PopoverPrimitive.Root data-slot="popover" {...props} />
}

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

function PopoverContent({
  className,
  align = "center",
  sideOffset = 4,
  ...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
  return (
    <PopoverPrimitive.Portal>
      <PopoverPrimitive.Content
        data-slot="popover-content"
        align={align}
        sideOffset={sideOffset}
        className={cn(
          "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
          className
        )}
        {...props}
      />
    </PopoverPrimitive.Portal>
  )
}

function PopoverAnchor({
  ...props
}: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
  return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />
}

export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
```

## Examples

### Popover

The canonical dimensions form — a small panel of `Label` + `Input` rows anchored to a trigger.

**Install:**

```bash
npx shadcn@latest add @designrevision/popover-demo-01
```

**Dependencies:** popover, button, input, label

```tsx
// components/ui/popover-demo-01.tsx
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

const fields = [
  { id: "width", label: "Width", value: "100%" },
  { id: "maxWidth", label: "Max. width", value: "300px" },
  { id: "height", label: "Height", value: "25px" },
  { id: "maxHeight", label: "Max. height", value: "none" },
]

export default function PopoverDemo01() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline">Open popover</Button>
      </PopoverTrigger>
      <PopoverContent className="w-80">
        <div className="grid gap-4">
          <div className="space-y-1.5">
            <h4 className="font-medium leading-none">Dimensions</h4>
            <p className="text-muted-foreground text-sm">
              Set the dimensions for the layer.
            </p>
          </div>
          <div className="grid gap-2">
            {fields.map((field) => (
              <div
                key={field.id}
                className="grid grid-cols-3 items-center gap-4"
              >
                <Label htmlFor={field.id}>{field.label}</Label>
                <Input
                  id={field.id}
                  defaultValue={field.value}
                  className="col-span-2 h-8"
                />
              </div>
            ))}
          </div>
        </div>
      </PopoverContent>
    </Popover>
  )
}
```

---

### Controlled form

A **controlled** popover holding a rename form that closes itself on submit or cancel.

**Install:**

```bash
npx shadcn@latest add @designrevision/popover-form-01
```

**Dependencies:** popover, button, input, label

```tsx
// components/ui/popover-form-01.tsx
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

export default function PopoverForm01() {
  const [open, setOpen] = React.useState(false)
  const [name, setName] = React.useState("Project Alpha")

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button variant="outline">Rename project</Button>
      </PopoverTrigger>
      <PopoverContent className="w-80">
        <form
          onSubmit={(event) => {
            event.preventDefault()
            setOpen(false)
          }}
          className="grid gap-3"
        >
          <div className="space-y-1.5">
            <h4 className="font-medium leading-none">Rename project</h4>
            <p className="text-muted-foreground text-sm">
              Give your project a new name.
            </p>
          </div>
          <div className="grid gap-2">
            <Label htmlFor="project-name">Name</Label>
            <Input
              id="project-name"
              value={name}
              onChange={(event) => setName(event.target.value)}
            />
          </div>
          <div className="flex justify-end gap-2">
            <Button
              type="button"
              variant="ghost"
              size="sm"
              onClick={() => setOpen(false)}
            >
              Cancel
            </Button>
            <Button type="submit" size="sm">
              Save
            </Button>
          </div>
        </form>
      </PopoverContent>
    </Popover>
  )
}
```

---

### Positioning

Open from any side with the `side` prop, fine-tuned by `align` and `sideOffset`.

**Install:**

```bash
npx shadcn@latest add @designrevision/popover-align-01
```

**Dependencies:** popover, button

```tsx
// components/ui/popover-align-01.tsx
import { Button } from "@/components/ui/button"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

const sides = ["top", "right", "bottom", "left"] as const

export default function PopoverAlign01() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      {sides.map((side) => (
        <Popover key={side}>
          <PopoverTrigger asChild>
            <Button variant="outline" className="capitalize">
              {side}
            </Button>
          </PopoverTrigger>
          <PopoverContent side={side} align="center" sideOffset={8} className="w-auto">
            <p className="text-sm">
              Opens on the <code>{side}</code> — <code>side="{side}"</code>.
            </p>
          </PopoverContent>
        </Popover>
      ))}
    </div>
  )
}
```

---

### Settings panel

A notification-settings panel with labelled [Switch](/components/switch) rows — the gear-icon popover.

**Install:**

```bash
npx shadcn@latest add @designrevision/popover-settings-01
```

**Dependencies:** lucide-react, popover, button, label, switch

```tsx
// components/ui/popover-settings-01.tsx
import { Settings2Icon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
import { Switch } from "@/components/ui/switch"

const rows = [
  { id: "comments", label: "Comments", desc: "Replies to your posts", checked: true },
  { id: "mentions", label: "Mentions", desc: "When someone @mentions you", checked: true },
  { id: "marketing", label: "Marketing", desc: "News and product updates", checked: false },
]

export default function PopoverSettings01() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="outline" size="icon" aria-label="Notification settings">
          <Settings2Icon />
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-72" align="end">
        <div className="grid gap-4">
          <div className="space-y-1.5">
            <h4 className="font-medium leading-none">Notifications</h4>
            <p className="text-muted-foreground text-sm">
              Choose what you want to hear about.
            </p>
          </div>
          <div className="grid gap-3">
            {rows.map((row) => (
              <div
                key={row.id}
                className="flex items-center justify-between gap-4"
              >
                <div className="space-y-0.5">
                  <Label htmlFor={row.id}>{row.label}</Label>
                  <p className="text-muted-foreground text-xs">{row.desc}</p>
                </div>
                <Switch id={row.id} defaultChecked={row.checked} />
              </div>
            ))}
          </div>
        </div>
      </PopoverContent>
    </Popover>
  )
}
```

---

### Notifications panel

A bell-icon trigger with an unread badge opening a list of notifications and a "mark all read" action.

**Install:**

```bash
npx shadcn@latest add @designrevision/popover-notifications-01
```

**Dependencies:** lucide-react, popover, button

```tsx
// components/ui/popover-notifications-01.tsx
import { BellIcon, CheckCheckIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

const notifications = [
  { id: 1, title: "Ada commented on your post", time: "2m ago", unread: true },
  { id: 2, title: "New sign-in from Chrome on macOS", time: "1h ago", unread: true },
  { id: 3, title: "Your export is ready to download", time: "3h ago", unread: false },
]

export default function PopoverNotifications01() {
  const unread = notifications.filter((n) => n.unread).length

  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          size="icon"
          className="relative"
          aria-label="Notifications"
        >
          <BellIcon />
          {unread > 0 && (
            <span className="bg-primary text-primary-foreground absolute -top-1 -right-1 flex size-4 items-center justify-center rounded-full text-[10px] font-medium">
              {unread}
            </span>
          )}
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-80 p-0" align="end">
        <div className="flex items-center justify-between border-b px-4 py-3">
          <h4 className="font-medium">Notifications</h4>
          <Button variant="ghost" size="sm" className="h-7 gap-1 text-xs">
            <CheckCheckIcon className="size-3.5" />
            Mark all read
          </Button>
        </div>
        <div className="grid">
          {notifications.map((n) => (
            <div
              key={n.id}
              className="flex items-start gap-3 border-b px-4 py-3 last:border-0"
            >
              <span
                className={
                  n.unread
                    ? "bg-primary mt-1.5 size-2 shrink-0 rounded-full"
                    : "mt-1.5 size-2 shrink-0"
                }
              />
              <div className="grid gap-0.5">
                <p className="text-sm">{n.title}</p>
                <p className="text-muted-foreground text-xs">{n.time}</p>
              </div>
            </div>
          ))}
        </div>
      </PopoverContent>
    </Popover>
  )
}
```

---

### Profile card

A @mention link opening a profile card with avatar, bio, a follow button, and a joined date.

**Install:**

```bash
npx shadcn@latest add @designrevision/popover-profile-01
```

**Dependencies:** lucide-react, popover, button, avatar

```tsx
// components/ui/popover-profile-01.tsx
import { CalendarIcon } from "lucide-react"

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

export default function PopoverProfile01() {
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button variant="link" className="px-0">
          @ada
        </Button>
      </PopoverTrigger>
      <PopoverContent className="w-80">
        <div className="flex gap-4">
          <Avatar className="size-12">
            <AvatarFallback>AL</AvatarFallback>
          </Avatar>
          <div className="grid gap-1.5">
            <div className="flex items-center justify-between gap-2">
              <div>
                <h4 className="font-medium leading-none">Ada Lovelace</h4>
                <p className="text-muted-foreground text-sm">@ada</p>
              </div>
              <Button size="sm" className="h-7">
                Follow
              </Button>
            </div>
            <p className="text-sm">
              The first programmer. Writes about analytical engines and
              abstraction.
            </p>
            <div className="text-muted-foreground flex items-center gap-1 text-xs">
              <CalendarIcon className="size-3.5" />
              Joined December 1842
            </div>
          </div>
        </div>
      </PopoverContent>
    </Popover>
  )
}
```

---

### Info / help

A "?" icon beside a field label opening a short explanatory popover — inline contextual help.

**Install:**

```bash
npx shadcn@latest add @designrevision/popover-help-01
```

**Dependencies:** lucide-react, popover, button, label

```tsx
// components/ui/popover-help-01.tsx
import { HelpCircleIcon } from "lucide-react"

import { Button } from "@/components/ui/button"
import { Label } from "@/components/ui/label"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"

export default function PopoverHelp01() {
  return (
    <div className="flex items-center gap-2">
      <Label htmlFor="api-key">API key</Label>
      <Popover>
        <PopoverTrigger asChild>
          <Button
            variant="ghost"
            size="icon"
            className="text-muted-foreground size-6"
            aria-label="What is an API key?"
          >
            <HelpCircleIcon className="size-4" />
          </Button>
        </PopoverTrigger>
        <PopoverContent side="top" className="w-72 text-sm">
          <p>
            Your <strong>secret API key</strong> authenticates requests. Keep it
            private — anyone with it can act on your behalf. You can rotate it
            any time from Settings.
          </p>
        </PopoverContent>
      </Popover>
    </div>
  )
}
```
