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.

Installation

1 Configure registry — once per project
{
  "registries": {
    "@designrevision": "https://registry.designrevision.com/r/{name}.json"
  }
}

Add to your existing components.json. Requires shadcn/ui v2.3+.

2 Install the component

Packages

@radix-ui/react-popover
utils

Props

side = "bottom"
"top" | "right" | "bottom" | "left"

Which side of the trigger the content opens on (on PopoverContent).

align = "center"
"start" | "center" | "end"

Alignment against the trigger (on PopoverContent).

sideOffset = 4
number

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).

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).

Copied!
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.

Packages

popover
button
input
label

Props

No props documented yet.

Copied!
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.

Packages

popover
button
input
label

Props

No props documented yet.

Copied!
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`.

Packages

popover
button

Props

No props documented yet.

Copied!
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.

Packages

lucide-react
popover
button
label
switch

Props

No props documented yet.

Copied!
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.

Packages

lucide-react
popover
button

Props

No props documented yet.

Copied!
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.

Packages

lucide-react
popover
button
avatar

Props

No props documented yet.

Copied!
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.

Packages

lucide-react
popover
button
label

Props

No props documented yet.

Copied!
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>
  )
}

Frequently asked questions

Yes — it is the canonical shadcn/ui popover, built on Radix Popover with four parts (Popover, PopoverTrigger, PopoverContent, PopoverAnchor). Official ships one dimensions-form demo; we add controlled forms, positioning, and settings, notifications, and profile panels.
A Popover is a generic floating panel for arbitrary content — a form, a settings group, a profile card. A [Dropdown Menu](/components/dropdown-menu) is a list of commands with menu keyboard semantics (arrow-key roving focus, type-ahead). If your panel is a menu of actions, use Dropdown Menu; if it holds fields or rich content, use a Popover.
A [Tooltip](/components/tooltip) appears on hover/focus, is non-interactive, and is for a short hint. A Popover opens on click, can contain interactive content (inputs, buttons, links), and traps focus while open. Use a Tooltip to label something; use a Popover when the user needs to do something inside it.
Set side ("top" | "right" | "bottom" | "left", default "bottom") and align ("start" | "center" | "end", default "center") on PopoverContent, and sideOffset for the gap in pixels. Radix flips and shifts the panel automatically to stay in the viewport. The Positioning example shows all four sides.
Control the open state with open / onOpenChange and call setOpen(false) in your submit handler (and on a Cancel button). The Controlled form example does exactly this. You can also wrap a button in PopoverClose for a declarative close.
By default a popover closes on outside-click and Escape. To keep it open, pass onInteractOutside={(e) => e.preventDefault()} (and/or onEscapeKeyDown) to PopoverContent — useful when the panel contains a step the user must finish.
PopoverAnchor lets the popover position itself against a different element than the trigger. Wrap the element you want it anchored to in PopoverAnchor and keep the PopoverTrigger elsewhere — handy when a small button opens a panel that should align to a larger field or row.