apps/docs/content/docs/react/migration/(components)/popover.mdx
In v2, Popover used separate components:
import { Popover, PopoverTrigger, PopoverContent, Button } from "@heroui/react";
export default function App() {
return (
<Popover placement="right">
<PopoverTrigger>
<Button>Open</Button>
</PopoverTrigger>
<PopoverContent>
<div>Content</div>
</PopoverContent>
</Popover>
);
}
In v3, Popover uses compound components:
import { Popover, Button } from "@heroui/react";
export default function App() {
return (
<Popover>
<Button>Open</Button>
<Popover.Content>
<Popover.Dialog>
<Popover.Heading>Title</Popover.Heading>
<div>Content</div>
</Popover.Dialog>
</Popover.Content>
</Popover>
);
}
v2: Separate components (Popover, PopoverTrigger, PopoverContent)
v3: Compound components (Popover, Popover.Trigger, Popover.Content, Popover.Dialog, Popover.Heading, Popover.Arrow)
| v2 Prop | v3 Location | Notes |
|---|---|---|
placement | placement (on Content) | Moved to Popover.Content |
offset | offset (on Content) | Moved to Popover.Content |
shouldFlip | shouldFlip (on Content) | Moved to Popover.Content |
isOpen / defaultOpen / onOpenChange | Same (on root) | Controlled state stays on root Popover |
showArrow | — | Use Popover.Arrow component |
size | — | Removed (use Tailwind CSS) |
color | — | Removed (use Tailwind CSS) |
radius | — | Removed (use Tailwind CSS) |
shadow | — | Removed (use Tailwind CSS) |
backdrop | — | Removed |
motionProps | — | Removed (animations handled differently) |
classNames | — | Use className on each part |
onClose | — | Use onOpenChange((open) => { if (!open) { ... } }) instead |
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx <Popover showArrow> <PopoverTrigger><Button>Open</Button></PopoverTrigger> <PopoverContent><div>Content</div></PopoverContent> </Popover> <Popover placement="top"> <PopoverTrigger><Button>Open</Button></PopoverTrigger> <PopoverContent><div>Content</div></PopoverContent> </Popover> <Popover offset={10}> <PopoverTrigger><Button>Open</Button></PopoverTrigger> <PopoverContent><div>Content</div></PopoverContent> </Popover>
</Tab>
<Tab value="v3">
tsx <Popover> <Button>Open</Button> <Popover.Content> <Popover.Dialog> <Popover.Arrow /> <div>Content</div> </Popover.Dialog> </Popover.Content> </Popover> <Popover> <Button>Open</Button> <Popover.Content placement="top"> <Popover.Dialog> <Popover.Arrow /> <div>Content</div> </Popover.Dialog> </Popover.Content> </Popover> <Popover> <Button>Open</Button> <Popover.Content offset={10}> <Popover.Dialog> <div>Content</div> </Popover.Dialog> </Popover.Content> </Popover>
</Tab>
</Tabs>
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx <PopoverContent> <div className="px-1 py-2"> <div className="text-small font-bold">Title</div> <div className="text-tiny">Content</div> </div> </PopoverContent>
</Tab>
<Tab value="v3">
tsx <Popover.Content> <Popover.Dialog> <Popover.Heading>Title</Popover.Heading> <p className="text-muted mt-2 text-sm">Content</p> </Popover.Dialog> </Popover.Content>
</Tab>
</Tabs>
<Tabs items={["v2", "v3"]}> <Tab value="v2"> ```tsx import { useState } from "react";
const [isOpen, setIsOpen] = useState(false);
<Popover isOpen={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger>
<Button>Open</Button>
</PopoverTrigger>
<PopoverContent>
<div>Content</div>
</PopoverContent>
</Popover>
```
const [isOpen, setIsOpen] = useState(false);
<Popover isOpen={isOpen} onOpenChange={setIsOpen}>
<Button>Open</Button>
<Popover.Content>
<Popover.Dialog>
<div>Content</div>
</Popover.Dialog>
</Popover.Content>
</Popover>
```
<Tabs items={["v2", "v3"]}>
<Tab value="v2">
tsx <PopoverTrigger> <CustomButton>Custom</CustomButton> </PopoverTrigger>
</Tab>
<Tab value="v3">
tsx <Popover.Trigger> <CustomButton>Custom</CustomButton> </Popover.Trigger>
</Tab>
</Tabs>
The v3 Popover follows this structure:
Popover (Root)
├── Popover.Trigger (optional, or use Button directly)
└── Popover.Content
└── Popover.Dialog
├── Popover.Arrow (optional)
├── Popover.Heading (optional)
└── Content
In v2, the arrow was controlled by the showArrow boolean prop on the root Popover. In v3, Popover.Arrow is a dedicated component placed inside Popover.Content, giving you full control over its rendering:
<Popover>
<Button>Open</Button>
<Popover.Content>
<Popover.Arrow className="custom-arrow" />
<Popover.Dialog>
<div>Content</div>
</Popover.Dialog>
</Popover.Content>
</Popover>
Popover.Arrow also accepts a render prop for fully custom arrow rendering.
Controlled open state remains on the root Popover component, using the same prop names as v2:
| Prop | Type | Default | Description |
|---|---|---|---|
isOpen | boolean | - | Controls popover visibility (controlled) |
defaultOpen | boolean | false | Initial open state (uncontrolled) |
onOpenChange | (isOpen: boolean) => void | - | Called when open state changes |
Popover.Content and Popover.Arrow both support a render prop that allows you to override the default DOM element with a custom render function for advanced use cases.
Popover.Content, Popover.Dialog, etc.)Popover.Trigger or Button directlyPopover.DialogshowArrow prop removed - use Popover.Arrow componentPopover.Heading component for titlesplacement, offset, shouldFlip moved to Popover.Contentsize, color, radius, shadow - use Tailwind CSSbackdrop prop removedmotionProps removed, animations handled differentlyclassName props on individual components