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.
Installation
{
"registries": {
"@designrevision": "https://registry.designrevision.com/r/{name}.json"
}
}
Add to your existing components.json. Requires shadcn/ui v2.3+.
Packages
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).
"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
Props
No props documented yet.
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
Props
No props documented yet.
"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
Props
No props documented yet.
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
Props
No props documented yet.
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
Props
No props documented yet.
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
Props
No props documented yet.
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
Props
No props documented yet.
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>
)
}
The Shadcn Popover is the floating panel — the canonical shadcn/ui popover built on Radix, 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 — a menu of actions with roving-focus keyboard semantics. Use it when every row is a command.
- Tooltip — a hover hint, non-interactive, for a short label.
- 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 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 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.