Use Popover for contextual controls that do not need a full-screen interruption. PopoverContent includes positioning and a portal. Use Dialog for a longer focused task.
Live preview
import { Check, ExternalLink } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverContent,
PopoverDescription,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover";
const checks = ["Registry schema valid", "Next.js consumer built", "Vite consumer built"];
export default function PopoverDemo() {
return (
<Popover>
<PopoverTrigger render={<Button variant="noShadow" />}>View release status</PopoverTrigger>
<PopoverContent className="w-[min(22rem,calc(100vw-2rem))]">
<div className="flex items-start justify-between gap-4">
<div>
<PopoverTitle>Release candidate</PopoverTitle>
<PopoverDescription className="mt-1">
All required consumer checks completed successfully.
</PopoverDescription>
</div>
<span className="rounded-base border-2 border-border bg-main px-2 py-1 text-xs font-heading text-main-foreground">
Ready
</span>
</div>
<ul className="mt-4 divide-y-2 divide-border border-y-2 border-border">
{checks.map((check) => (
<li key={check} className="flex items-center gap-3 py-3 text-sm">
<Check className="size-4 shrink-0" aria-hidden="true" />
{check}
</li>
))}
</ul>
<a
href="/docs/registry"
className="mt-4 inline-flex items-center gap-2 font-heading underline underline-offset-4"
>
Review registry guidance
<ExternalLink className="size-4" aria-hidden="true" />
</a>
</PopoverContent>
</Popover>
);
}
#Installation
Complete the base setup. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/popover.jsonnpm install @base-ui/react@^1.9.0Create components/ui/popover.tsx:
"use client";
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
import type * as React from "react";
import { cn } from "@/lib/utils";
type PopoverContentProps = PopoverPrimitive.Popup.Props &
Pick<
PopoverPrimitive.Positioner.Props,
| "align"
| "alignOffset"
| "arrowPadding"
| "collisionAvoidance"
| "collisionBoundary"
| "collisionPadding"
| "disableAnchorTracking"
| "positionMethod"
| "side"
| "sideOffset"
| "sticky"
>;
function Popover(props: PopoverPrimitive.Root.Props) {
return <PopoverPrimitive.Root {...props} />;
}
function PopoverTrigger(props: PopoverPrimitive.Trigger.Props) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverContent({
align = "center",
alignOffset = 0,
arrowPadding = 0,
children,
className,
collisionAvoidance,
collisionBoundary,
collisionPadding = 0,
disableAnchorTracking,
positionMethod,
side = "bottom",
sideOffset = 4,
sticky,
...props
}: PopoverContentProps) {
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Positioner
align={align}
alignOffset={alignOffset}
arrowPadding={arrowPadding}
collisionAvoidance={collisionAvoidance}
collisionBoundary={collisionBoundary}
collisionPadding={collisionPadding}
disableAnchorTracking={disableAnchorTracking}
positionMethod={positionMethod}
side={side}
sideOffset={sideOffset}
sticky={sticky}
className="isolate z-50"
>
<PopoverPrimitive.Popup
data-slot="popover-content"
className={(state) =>
cn(
"z-50 w-72 origin-(--transform-origin) rounded-base border-2 border-border bg-main p-4 text-main-foreground outline-none duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-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 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
typeof className === "function" ? className(state) : className,
)
}
{...props}
>
{children}
</PopoverPrimitive.Popup>
</PopoverPrimitive.Positioner>
</PopoverPrimitive.Portal>
);
}
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="popover-header"
className={cn("flex flex-col gap-0.5 text-sm", className)}
{...props}
/>
);
}
function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
return (
<PopoverPrimitive.Title
data-slot="popover-title"
className={(state) =>
cn("font-heading", typeof className === "function" ? className(state) : className)
}
{...props}
/>
);
}
function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props) {
return (
<PopoverPrimitive.Description
data-slot="popover-description"
className={(state) =>
cn("text-main-foreground", typeof className === "function" ? className(state) : className)
}
{...props}
/>
);
}
export { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger };
The trigger example additionally uses Button.
#Usage
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverTitle,
PopoverDescription,
} from "@/components/ui/popover";
export default function HelpPopover() {
return (
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Sharing help</PopoverTrigger>
<PopoverContent>
<PopoverTitle>Workspace sharing</PopoverTitle>
<PopoverDescription>Only invited members can open this workspace.</PopoverDescription>
<a href="/docs" className="underline">
Read the guide
</a>
</PopoverContent>
</Popover>
);
}#API reference
| Prop / part | Purpose | Default |
|---|---|---|
Popover.open / onOpenChange | Controlled visibility | Uncontrolled |
Popover.defaultOpen | Initial visibility | false |
PopoverTrigger.render | Trigger button composition | Primitive button |
PopoverContent.side / align | Preferred placement | bottom / center |
PopoverContent.sideOffset | Trigger-to-popup gap | 4 |
PopoverContent.initialFocus / finalFocus | Focus destinations | Base UI behavior |
PopoverContent.alignOffset / arrowPadding / collisionPadding | Alignment, arrow, and collision-boundary spacing, in pixels | 0 |
#Accessibility
Name the popup with PopoverTitle and explain its purpose when needed. Keep controls reachable and verify Escape dismissal and focus return. Do not move essential instructions into a popup users must discover before they can complete a form.