Overview
Get Started
import { Button } from "@/components/ui/button";
import { Field } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import {
Popover,
PopoverDescription,
PopoverPopup,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover";
import { Textarea } from "@/components/ui/textarea";
export function PopoverDemo() {
return (
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Open Popover</PopoverTrigger>
<PopoverPopup className="w-xs">
<PopoverTitle>Send us feedback</PopoverTitle>
<PopoverDescription>Let us know how we can improve.</PopoverDescription>
<Form className="mt-4">
<Field>
<Textarea aria-label="Send feedback" id="feedback" placeholder="How can we improve?" />
</Field>
<Button type="submit">Send feedback</Button>
</Form>
</PopoverPopup>
</Popover>
);
}Installation
import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
import { cn } from "@/lib/cn";
const popoverCreateHandle = PopoverPrimitive.createHandle;
const Popover = PopoverPrimitive.Root;
function PopoverTrigger(props: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
}
function PopoverPortal(props: React.ComponentProps<typeof PopoverPrimitive.Portal>) {
return <PopoverPrimitive.Portal data-slot="popover-portal" {...props} />;
}
function PopoverPositioner({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Positioner>) {
return (
<PopoverPrimitive.Positioner
data-slot="popover-positioner"
sideOffset={sideOffset}
className={cn(
"z-50 h-(--positioner-height) max-h-(--available-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom,transform] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)]",
className
)}
{...props}
/>
);
}
function PopoverViewport({
className,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Viewport>) {
return (
<PopoverPrimitive.Viewport
data-slot="popover-viewport"
className={cn(
"[--viewport-inline-padding:--spacing(4)]",
"relative size-full overflow-clip px-(--viewport-inline-padding) py-4 data-instant:transition-none",
"**:data-current:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding))] **:data-current:translate-x-0 **:data-current:opacity-100 **:data-current:transition-[translate,opacity] **:data-current:duration-200 **:data-current:ease-[cubic-bezier(0.22,1,0.36,1)]",
"**:data-previous:w-[calc(var(--popup-width)-2*var(--viewport-inline-padding))] **:data-previous:translate-x-0 **:data-previous:opacity-100 **:data-previous:transition-[translate,opacity] **:data-previous:duration-200 **:data-previous:ease-[cubic-bezier(0.22,1,0.36,1)]",
"data-[activation-direction~='left']:[&_[data-current][data-starting-style]]:-translate-x-1/2 data-[activation-direction~='right']:[&_[data-current][data-starting-style]]:translate-x-1/2 data-[activation-direction~='left']:[&_[data-current][data-starting-style]]:opacity-0 data-[activation-direction~='right']:[&_[data-current][data-starting-style]]:opacity-0",
"data-[activation-direction~='left']:[&_[data-previous][data-ending-style]]:translate-x-1/2 data-[activation-direction~='right']:[&_[data-previous][data-ending-style]]:-translate-x-1/2 data-[activation-direction~='left']:[&_[data-previous][data-ending-style]]:opacity-0 data-[activation-direction~='right']:[&_[data-previous][data-ending-style]]:opacity-0",
className
)}
{...props}
/>
);
}
function PopoverPopup({
children,
className,
showArrow = false,
tooltipStyle = false,
portalProps,
positionerProps,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Popup> & {
showArrow?: boolean;
tooltipStyle?: boolean;
portalProps?: React.ComponentProps<typeof PopoverPortal>;
positionerProps?: React.ComponentProps<typeof PopoverPrimitive.Positioner>;
}) {
return (
<PopoverPortal {...portalProps}>
<PopoverPositioner sideOffset={showArrow ? 8 : 4} {...positionerProps}>
<PopoverPrimitive.Popup
data-slot="popover-popup"
className={cn(
"relative h-(--popup-height,auto) w-(--popup-width,auto) origin-(--transform-origin) rounded-2xl bg-popover p-4 text-popover-foreground shadow-lg/5 outline outline-border -outline-offset-1 transition-[width,height,opacity,scale] data-ending-style:scale-90 data-starting-style:scale-90 data-ending-style:opacity-0 data-starting-style:opacity-0",
"duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] data-instant:transition-none",
tooltipStyle && "w-fit text-balance rounded-md px-2 py-1 text-xs shadow-md/5",
className
)}
{...props}
>
{showArrow && <PopoverArrow />}
{children}
</PopoverPrimitive.Popup>
</PopoverPositioner>
</PopoverPortal>
);
}
function PopoverClose(props: React.ComponentProps<typeof PopoverPrimitive.Close>) {
return <PopoverPrimitive.Close data-slot="popover-close" {...props} />;
}
function PopoverTitle({
className,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Title>) {
return (
<PopoverPrimitive.Title
data-slot="popover-title"
className={cn("font-medium", className)}
{...props}
/>
);
}
function PopoverDescription({
className,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Description>) {
return (
<PopoverPrimitive.Description
data-slot="popover-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
);
}
function PopoverArrow({ children, ...props }: React.ComponentProps<typeof PopoverPrimitive.Arrow>) {
return (
<PopoverPrimitive.Arrow
data-slot="popover-arrow"
className={cn(
"flex transition-[top,right,bottom,left] duration-200 ease-[cubic-bezier(0.22,1,0.36,1)] data-[side=bottom]:-top-2 data-[side=left]:-right-3.25 data-[side=top]:-bottom-2 data-[side=right]:-left-3.25 data-[side=bottom]:rotate-0 data-[side=left]:rotate-90 data-[side=right]:-rotate-90 data-[side=top]:rotate-180 data-instant:transition-none"
)}
{...props}
>
{children || <ArrowSvg />}
</PopoverPrimitive.Arrow>
);
}
function ArrowSvg(props: React.ComponentProps<"svg">) {
return (
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" {...props}>
<path
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
className="fill-popover"
/>
<path
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
className="fill-border"
/>
</svg>
);
}
export {
PopoverPrimitive,
Popover,
PopoverTrigger,
PopoverPortal,
PopoverPositioner,
PopoverViewport,
PopoverPopup,
PopoverClose,
PopoverTitle,
PopoverDescription,
PopoverArrow,
popoverCreateHandle,
};Usage
import {
Popover,
PopoverDescription,
PopoverPopup,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover";<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Open popover</PopoverTrigger>
<PopoverPopup>
<PopoverTitle>Title</PopoverTitle>
<PopoverDescription>Description goes here.</PopoverDescription>
</PopoverPopup>
</Popover>Examples
Align
Change the alignment by setting the positionerProps align property to start, center, or end.
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverDescription,
PopoverPopup,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover";
export function PopoverDemo() {
return (
<div className="flex items-center gap-5">
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Start</PopoverTrigger>
<PopoverPopup positionerProps={{ align: "start" }}>
<PopoverTitle>Title</PopoverTitle>
<PopoverDescription>Description goes here.</PopoverDescription>
</PopoverPopup>
</Popover>
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Center</PopoverTrigger>
<PopoverPopup positionerProps={{ align: "center" }}>
<PopoverTitle>Title</PopoverTitle>
<PopoverDescription>Description goes here.</PopoverDescription>
</PopoverPopup>
</Popover>
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>End</PopoverTrigger>
<PopoverPopup positionerProps={{ align: "end" }}>
<PopoverTitle>Title</PopoverTitle>
<PopoverDescription>Description goes here.</PopoverDescription>
</PopoverPopup>
</Popover>
</div>
);
}Side
Change the side by setting the positionerProps side property to top, right, bottom, left, inline-start or inline-end.
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverDescription,
PopoverPopup,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover";
export function PopoverDemo() {
return (
<div className="grid grid-cols-3 items-center gap-5">
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Top</PopoverTrigger>
<PopoverPopup positionerProps={{ side: "top" }}>
<PopoverTitle>Title</PopoverTitle>
<PopoverDescription>Description goes here.</PopoverDescription>
</PopoverPopup>
</Popover>
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Bottom</PopoverTrigger>
<PopoverPopup positionerProps={{ side: "bottom" }}>
<PopoverTitle>Title</PopoverTitle>
<PopoverDescription>Description goes here.</PopoverDescription>
</PopoverPopup>
</Popover>
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Left</PopoverTrigger>
<PopoverPopup positionerProps={{ side: "left" }}>
<PopoverTitle>Title</PopoverTitle>
<PopoverDescription>Description goes here.</PopoverDescription>
</PopoverPopup>
</Popover>
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Right</PopoverTrigger>
<PopoverPopup positionerProps={{ side: "right" }}>
<PopoverTitle>Title</PopoverTitle>
<PopoverDescription>Description goes here.</PopoverDescription>
</PopoverPopup>
</Popover>
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Inline start</PopoverTrigger>
<PopoverPopup positionerProps={{ side: "inline-start" }}>
<PopoverTitle>Title</PopoverTitle>
<PopoverDescription>Description goes here.</PopoverDescription>
</PopoverPopup>
</Popover>
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Inline end</PopoverTrigger>
<PopoverPopup positionerProps={{ side: "inline-end" }}>
<PopoverTitle>Title</PopoverTitle>
<PopoverDescription>Description goes here.</PopoverDescription>
</PopoverPopup>
</Popover>
</div>
);
}With close button
import { IconBell, IconX } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverClose,
PopoverDescription,
PopoverPopup,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover";
export function PopoverDemo() {
return (
<Popover>
<PopoverTrigger render={<Button variant="outline" size="icon" />}>
<IconBell />
</PopoverTrigger>
<PopoverPopup className="w-full max-w-xs">
<PopoverClose
aria-label="Close"
className="absolute end-2 top-2"
render={<Button size="icon-sm" variant="ghost" />}
>
<IconX />
</PopoverClose>
<div>
<PopoverTitle>Notifications</PopoverTitle>
<PopoverDescription>You are all caught up. Good job!</PopoverDescription>
</div>
<PopoverClose render={<Button variant="outline" className="mt-5" />}>Close</PopoverClose>
</PopoverPopup>
</Popover>
);
}With arrow
To add an arrow pointing to the trigger, set showArrow to true on the PopoverPopup.
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverDescription,
PopoverPopup,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover";
export function PopoverDemo() {
return (
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>Open Popover</PopoverTrigger>
<PopoverPopup showArrow positionerProps={{ className: "max-w-xs w-full", side: "top" }}>
<PopoverTitle>Title</PopoverTitle>
<PopoverDescription>Description goes here.</PopoverDescription>
</PopoverPopup>
</Popover>
);
}Tooltip style
Pass tooltipStyle to the PopoverPopup to make the popover look like a tooltip. Pass openOnHover to the PopoverTrigger to open the popover on hover instead of click.
"use client";
import { IconInfoCircle } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import { InputGroup, InputGroupAddon, InputGroupInput } from "@/components/ui/input-group";
import { Popover, PopoverPopup, PopoverTrigger } from "@/components/ui/popover";
export default function Particle() {
return (
<InputGroup className="max-w-xs">
<InputGroupInput aria-label="Password" placeholder="Password" type="password" />
<InputGroupAddon align="inline-end">
<Popover>
<PopoverTrigger
openOnHover
render={<Button aria-label="Password requirements" size="icon-xs" variant="ghost" />}
>
<IconInfoCircle />
</PopoverTrigger>
<PopoverPopup tooltipStyle positionerProps={{ side: "top" }}>
<p>Min. 8 characters</p>
</PopoverPopup>
</Popover>
</InputGroupAddon>
</InputGroup>
);
}Animated
You can create animated popovers that smoothly transition between different triggers using detached triggers. This pattern allows multiple triggers to share a single popover popup, with automatic animations for position, size, and content changes.
To create detached triggers:
- Create a handle using
popoverCreateHandle - Attach the same handle to multiple
PopoverTriggercomponents - Each trigger provides a
payloadprop containing the content component - Use a single
Popovercomponent with the handle to render the popup
"use client";
import { IconBell, IconList, IconUserCircle } from "@tabler/icons-react";
import { cn } from "@/lib/cn";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button, buttonVariants } from "@/components/ui/button";
import {
Popover,
PopoverDescription,
PopoverPopup,
PopoverTitle,
PopoverTrigger,
PopoverViewport,
popoverCreateHandle,
} from "@/components/ui/popover";
const popoverHandler = popoverCreateHandle<React.ComponentType>();
export function PopoverDemo() {
return (
<div className="flex items-center gap-3">
<PopoverTrigger
handle={popoverHandler}
payload={NotificationsContent}
render={<Button variant="outline" size="icon" />}
>
<IconBell />
</PopoverTrigger>
<PopoverTrigger
handle={popoverHandler}
payload={ActivityContent}
render={<Button variant="outline" size="icon" />}
>
<IconList />
</PopoverTrigger>
<PopoverTrigger
handle={popoverHandler}
payload={ProfileContent}
render={<Button variant="outline" size="icon" />}
>
<IconUserCircle />
</PopoverTrigger>
<Popover handle={popoverHandler}>
{({ payload: Payload }) => (
<PopoverPopup showArrow className="p-0">
<PopoverViewport>{Payload !== undefined && <Payload />}</PopoverViewport>
</PopoverPopup>
)}
</Popover>
</div>
);
}
function NotificationsContent() {
return (
<>
<PopoverTitle>Notifications</PopoverTitle>
<PopoverDescription>You are all caught up. Good job!</PopoverDescription>
</>
);
}
function ActivityContent() {
return (
<>
<PopoverTitle>Activity</PopoverTitle>
<PopoverDescription>Nothing interesting happened recently.</PopoverDescription>
</>
);
}
function ProfileContent() {
return (
<div>
<div className="grid grid-cols-[auto_auto] gap-x-4">
<Avatar className="size-12">
<AvatarImage src="https://github.com/zbmzubayer.png" alt="Zbm Zubayer" />
<AvatarFallback>ZM</AvatarFallback>
</Avatar>
<div>
<PopoverTitle>ZBM Zubayer</PopoverTitle>
<span className="text-muted-foreground text-sm">Pro plan</span>
</div>
</div>
<div className="mt-2 flex flex-col gap-2 border-muted border-t pt-2 text-sm">
<a href="#" className={cn(buttonVariants({ variant: "secondary" }))}>
Profile settings
</a>
<Button variant="danger-soft">Log out</Button>
</div>
</div>
);
}