Popover
freePopover floats details, quick forms, and secondary controls next to a trigger.
@floating-ui/react
Preview
Dark
import Button from '@/components/ui/Button';
import Popover from '@/components/ui/Popover';
export default function UsageDemo() {
return (
<div className="flex justify-center">
<Popover title="What's new" width={350} className="p-0">
<div className="p-2">
<img
src="/img/thumbs/misc/img-10.png"
alt=""
className="rounded-lg"
/>
</div>
<div className="flex flex-col gap-1 p-4">
<div className="font-semibold">
New updates just landed
</div>
<p className="text-muted-foreground">
Explore the latest features and find quick answers in the help
center.
</p>
</div>
<div className="flex items-center justify-end gap-2 p-4">
<Button variant="ghost" size="sm">
Maybe later
</Button>
<Button variant="solid" size="sm">
Explore now
</Button>
</div>
</Popover>
</div>
);
}Installation
Add this component with the NateUI CLI.
npx nateui@latest add PopoverExamples
Basic
Preview
Dark
import { PiArrowUpRight, PiSquaresFour } from 'react-icons/pi'
import Popover from '@/components/ui/Popover';
export default function BasicDemo() {
return (
<div className="flex justify-center">
<Popover title="Components" width={260}>
<div className="flex flex-col gap-3">
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-muted">
<PiSquaresFour className="text-base text-foreground" />
</span>
<div className="flex flex-col gap-1">
<div className="font-semibold text-foreground">
Consistent by default
</div>
<p className="text-sm text-muted-foreground">
Shared components keep every screen aligned, so teams stop
rebuilding the same patterns from scratch.
</p>
</div>
<div className="border-t border-border" />
<a
href="#"
className="inline-flex items-center justify-end gap-1 text-sm font-medium hover:underline"
>
Learn more
<PiArrowUpRight className="text-sm" />
</a>
</div>
</Popover>
</div>
);
}Trigger
Preview
Dark
import { PiArrowUpRight, PiSquaresFour } from 'react-icons/pi'
import Popover from '@/components/ui/Popover';
const content = (
<div className="flex flex-col gap-3">
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-muted">
<PiSquaresFour className="text-base text-foreground" />
</span>
<div className="flex flex-col gap-1">
<div className="font-semibold text-foreground">
Consistent by default
</div>
<p className="text-sm text-muted-foreground">
Shared components keep every screen aligned, so teams stop
rebuilding the same patterns from scratch.
</p>
</div>
<div className="border-t border-border" />
<a
href="#"
className="inline-flex items-center justify-end gap-1 text-sm font-medium hover:underline"
>
Learn more
<PiArrowUpRight className="text-sm" />
</a>
</div>
);
export default function TriggerDemo() {
return (
<div className="flex flex-wrap justify-center gap-2">
<Popover title="Click" width={260}>
{content}
</Popover>
<Popover title="Hover" trigger="hover" width={260}>
{content}
</Popover>
</div>
);
}Custom Trigger
Preview
Dark
import { PiSealCheck } from 'react-icons/pi'
import Avatar from '@/components/ui/Avatar';
import Button from '@/components/ui/Button';
import Tag from '@/components/ui/Tag';
import Popover from '@/components/ui/Popover';
const stats = [
{ value: '4.8', label: 'rating' },
{ value: '$68K', label: 'earned' },
{ value: '$65/hr', label: 'rate' },
];
export default function CustomTriggerDemo() {
return (
<div className="flex justify-center">
<Popover
width={300}
className="p-0"
renderTrigger={
<button className="flex items-center gap-2">
<Avatar
size="sm"
src="/img/avatars/thumb-13.jpg"
alt="Maya Ortiz"
/>
<span className="flex flex-col text-start">
<span className="font-semibold text-foreground">Maya Ortiz</span>
<span className="text-muted-foreground">
Freelance UX Consultant
</span>
</span>
</button>
}
>
<div className="relative h-20 w-full overflow-hidden rounded-t-popover">
<img
src="/img/thumbs/misc/img-6.png"
alt=""
className="absolute inset-0 h-full w-full object-cover object-top"
/>
</div>
<div className="-mt-8 px-4 pb-4">
<div className="flex items-end justify-between">
<Avatar
size={64}
src="/img/avatars/thumb-13.jpg"
alt="Maya Ortiz"
className="border-4 border-popover"
/>
<Tag className="flex items-center gap-1">
<span className="h-2 w-2 rounded-full bg-success" />
Active now
</Tag>
</div>
<div className="mt-4 flex items-center gap-1">
<span className="font-semibold text-foreground">Maya Ortiz</span>
<span className="flex h-4 w-4 items-center justify-center text-xl">
<PiSealCheck className="fill-primary stroke-inverse-foreground " />
</span>
</div>
<p className="text-sm text-muted-foreground">Freelance UX Consultant</p>
<div className="mt-4 grid grid-cols-3 divide-x divide-border rounded-control py-2">
{stats.map((stat) => (
<div
key={stat.label}
className="flex flex-col items-center gap-0.5"
>
<span className="font-semibold text-foreground">
{stat.value}
</span>
<span className="text-xs text-muted-foreground">
{stat.label}
</span>
</div>
))}
</div>
<Button block variant="solid" className="mt-4">
Message
</Button>
</div>
</Popover>
</div>
);
}Placement
Preview
Dark
import { PiArrowUpRight, PiSquaresFour } from 'react-icons/pi'
import Popover from '@/components/ui/Popover';
const content = (
<div className="flex flex-col gap-3">
<span className="flex h-8 w-8 items-center justify-center rounded-lg bg-muted">
<PiSquaresFour className="text-base text-foreground" />
</span>
<div className="flex flex-col gap-1">
<div className="font-semibold text-foreground">
Consistent by default
</div>
<p className="text-sm text-muted-foreground">
Shared components keep every screen aligned, so teams stop
rebuilding the same patterns from scratch.
</p>
</div>
<div className="border-t border-border" />
<a
href="#"
className="inline-flex items-center justify-end gap-1 text-sm font-medium hover:underline"
>
Learn more
<PiArrowUpRight className="text-sm" />
</a>
</div>
);
export default function PlacementDemo() {
const popoverPlacements = [
{
title: 'Top start',
placement: 'top-start'
},
{
title: 'Top',
placement: 'top'
},
{
title: 'Top end',
placement: 'top-end'
},
{
title: 'Bottom start',
placement: 'bottom-start'
},
{
title: 'Bottom',
placement: 'bottom'
},
{
title: 'Bottom end',
placement: 'bottom-end'
},
{
title: 'Left start',
placement: 'left-start'
},
{
title: 'Left',
placement: 'left'
},
{
title: 'Left end',
placement: 'left-end'
},
{
title: 'Right start',
placement: 'right-start'
},
{
title: 'Right',
placement: 'right'
},
{
title: 'Right end',
placement: 'right-end'
}
] as const;
return (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
{popoverPlacements.map((placement) => (
<Popover
key={placement.placement}
title={placement.title}
placement={placement.placement}
width={260}
>
{content}
</Popover>
))}
</div>
);
}Controlled
Preview
Dark
import { useState } from 'react';
import Calendar from '@/components/ui/Calendar';
import Popover from '@/components/ui/Popover';
export default function ControlledDemo() {
const [open, setOpen] = useState(false);
const [date, setDate] = useState<Date | null>(null);
const onCalendarChange = (value: Date | null) => {
setDate(value);
setOpen(false);
};
return (
<div className="flex justify-center">
<Popover
title={date ? date.toDateString() : 'Select a date'}
open={open}
onOpenChange={setOpen}
placement="bottom-start"
width={280}
>
<Calendar value={date} onChange={onCalendarChange} />
</Popover>
</div>
);
}API
Popover renders a title (or a renderTrigger element) as the trigger and floats its children in a portalled panel. Open state is uncontrolled by default, or controlled with open and onOpenChange.
| Prop | Description | Type | Default |
|---|---|---|---|
title | Label for the default trigger. Rendered inside the built-in button when renderTrigger is not set. | string | ReactNode | - |
renderTrigger | Custom element used as the trigger, replacing the default button. | ReactNode | - |
children | Content rendered inside the floating panel. | ReactNode | - |
trigger | How the panel opens. | 'click' | 'hover' | 'click' |
placement | Side and alignment the panel opens from, relative to the trigger. | Placement | 'bottom' |
open | Controls the open state. Pair with onOpenChange. | boolean | - |
onOpenChange | Called when the open state should change. | (open: boolean) => void | - |
width | Width of the panel body. | number | string | - |
modal | Traps focus within the panel while open. | boolean | false |
className | Class names applied to the panel body. | string | - |
...nativeProps | Native div props applied to the floating panel. | Omit<ComponentProps<'div'>, 'title'> | - |
Types
type Placement =
| 'top'
| 'top-start'
| 'top-end'
| 'bottom'
| 'bottom-start'
| 'bottom-end'
| 'right'
| 'right-start'
| 'right-end'
| 'left'
| 'left-start'
| 'left-end';