- Accordion
- Alert
- Alert Dialog
- Autocomplete
- Avatar
- Badge
- Breadcrumb
- Button
- Card
- Checkbox
- Checkbox Group
- Collapsible
- Combobox
- Dialog
- EmptyNew
- Field
- Fieldset
- Form
- Frame
- Group
- Input
- Input GroupNew
- KbdNew
- Label
- Menu
- Meter
- Number Field
- Pagination
- Popover
- Preview Card
- Progress
- Radio Group
- Scroll Area
- Select
- Separator
- Sheet
- SkeletonNew
- Slider
- SpinnerNew
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Toggle Group
- Toolbar
- Tooltip
Popover
An accessible popup anchored to a button.
"use client";
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 default function Particle() {
return (
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>
Open Popover
</PopoverTrigger>
<PopoverPopup className="w-80">
<div className="mb-4">
<PopoverTitle className="text-base">Send us feedback</PopoverTitle>
<PopoverDescription>
Let us know how we can improve.
</PopoverDescription>
</div>
<Form>
<Field>
<Textarea
aria-label="Send feedback"
id="feedback"
placeholder="How can we improve?"
/>
</Field>
<Button type="submit">Send feedback</Button>
</Form>
</PopoverPopup>
</Popover>
);
}
Installation
pnpm dlx shadcn@latest add @coss/popover
Usage
import {
Popover,
PopoverClose,
PopoverDescription,
PopoverPopup,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover"<Popover>
<PopoverTrigger>Open Popover</PopoverTrigger>
<PopoverPopup>
<PopoverTitle>Popover Title</PopoverTitle>
<PopoverDescription>Popover Description</PopoverDescription>
<PopoverClose>Close</PopoverClose>
</PopoverPopup>
</Popover>Examples
With Close Button
"use client";
import { XIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverClose,
PopoverDescription,
PopoverPopup,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover";
export default function Particle() {
return (
<Popover>
<PopoverTrigger render={<Button variant="outline" />}>
Open Popover
</PopoverTrigger>
<PopoverPopup className="w-80">
<PopoverClose className="pointer-coarse:after:-inset-1 absolute end-2 top-2 inline-flex size-7 shrink-0 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-72 outline-none transition-[color,background-color,box-shadow,opacity] pointer-coarse:after:absolute hover:opacity-100 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0">
<XIcon />
<span className="sr-only">Close</span>
</PopoverClose>
<div className="mb-2">
<PopoverTitle className="text-base">Notifications</PopoverTitle>
<PopoverDescription>
You are all caught up. Good job!
</PopoverDescription>
</div>
<PopoverClose render={<Button variant="outline" />}>Close</PopoverClose>
</PopoverPopup>
</Popover>
);
}
Tooltip Style
Use the tooltipStyle prop to make a popover look like a tooltip. This is recommended when you have an info icon button whose only purpose is to show additional information. See the tooltip accessibility guidelines for best practices.
"use client";
import { InfoIcon } from "lucide-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>
<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"
/>
}
>
<InfoIcon />
</PopoverTrigger>
<PopoverPopup side="top" tooltipStyle>
<p>Min. 8 characters</p>
</PopoverPopup>
</Popover>
</InputGroupAddon>
</InputGroup>
);
}
Animated Popovers
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 { BellIcon, UserIcon } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverCreateHandle,
PopoverDescription,
PopoverPopup,
PopoverTitle,
PopoverTrigger,
} from "@/components/ui/popover";
const popoverHandle = PopoverCreateHandle<React.ComponentType>();
const NotificationsContent = () => {
return (
<>
<PopoverTitle className="text-base">Notifications</PopoverTitle>
<PopoverDescription>
You have no new notifications at this time.
</PopoverDescription>
</>
);
};
const ProfileContent = () => {
return (
<div className="w-48">
<div className="flex items-center gap-3">
<Avatar>
<AvatarImage
alt="Mark Andersson"
src="https://images.unsplash.com/photo-1543610892-0b1f7e6d8ac1?w=128&h=128&dpr=2&q=80"
/>
<AvatarFallback>MA</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<h4 className="line-clamp-1 font-medium text-sm">Mark Andersson</h4>
<div className="flex items-center gap-3 text-muted-foreground text-xs">
Product Designer
</div>
</div>
</div>
<Button className="mt-3 w-full" size="sm" variant="outline">
Log out
</Button>
</div>
);
};
export default function Particle() {
return (
<div className="flex gap-2">
<PopoverTrigger
handle={popoverHandle}
payload={NotificationsContent}
render={<Button size="icon" variant="outline" />}
>
<BellIcon aria-label="Notifications" />
</PopoverTrigger>
<PopoverTrigger
handle={popoverHandle}
payload={ProfileContent}
render={<Button size="icon" variant="outline" />}
>
<UserIcon aria-label="Profile" />
</PopoverTrigger>
<Popover handle={popoverHandle}>
{({ payload: Payload }) => (
<PopoverPopup className="min-w-none">
{Payload !== undefined && <Payload />}
</PopoverPopup>
)}
</Popover>
</div>
);
}
Comparing with Radix / shadcn
If you’re already familiar with Radix UI and shadcn/ui, this guide highlights the small differences and similarities so you can get started with coss ui quickly.
Quick Checklist
- Replace
asChild→renderonPopoverTriggerand closing buttons - Prefer
PopoverPopup;PopoverContentremains for legacy - If you used
asChildon any other parts, switch to therenderprop
Additional Notes
Base UI introduces PopoverTitle and PopoverDescription to structure headings and helper text inside the popup. Base UI also introduces a PopoverClose component for adding close buttons to the popup.
Comparison Example
<Popover>
<PopoverTrigger asChild>
<Button variant="outline">Open Popover</Button>
</PopoverTrigger>
<PopoverContent>
<h2>Popover Title</h2>
<p>Popover Description</p>
</PopoverContent>
</Popover><Popover>
<PopoverTrigger render={<Button variant="outline" />}>
Open Popover
</PopoverTrigger>
<PopoverPopup>
<PopoverTitle>Popover Title</PopoverTitle>
<PopoverDescription>Popover Description</PopoverDescription>
<PopoverClose render={<Button variant="ghost" />}>Close</PopoverClose>
</PopoverPopup>
</Popover>