packages/codemod/docs/POPOVER_MIGRATION.md
This guide explains how to migrate from Chakra UI v2 Popover components to the v3 compound component API with positioning enhancements.
In Chakra UI v3, the Popover component has been redesigned to use:
npx @chakra-ui/codemod@latest popover <path>
This will automatically transform all Popover components in your codebase.
import {
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverHeader,
PopoverTrigger,
} from "@chakra-ui/react"
;<Popover>
<PopoverTrigger>
<Button>Trigger</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverCloseButton />
<PopoverHeader>Confirmation!</PopoverHeader>
<PopoverBody>Are you sure?</PopoverBody>
</PopoverContent>
</Popover>
import { Button, Popover } from "@chakra-ui/react"
;<Popover.Root>
<Popover.Trigger asChild>
<Button>Trigger</Button>
</Popover.Trigger>
<Popover.Positioner>
<Popover.Content>
<Popover.Arrow />
<Popover.CloseTrigger />
<Popover.Title>Confirmation!</Popover.Title>
<Popover.Body>Are you sure?</Popover.Body>
</Popover.Content>
</Popover.Positioner>
</Popover.Root>
Key Changes:
Popover → Popover.RootPopover.*PopoverContent wrapped in Popover.PositionerasChild prop added to Popover.Triggerimport { Popover } from '@chakra-ui/react'| v2 Component | v3 Component | Notes |
|---|---|---|
Popover | Popover.Root | Root container |
PopoverTrigger | Popover.Trigger | Add asChild prop |
PopoverContent | Popover.Content | Wrap in Popover.Positioner |
PopoverHeader | Popover.Title | Name change |
PopoverBody | Popover.Body | - |
PopoverFooter | Popover.Footer | - |
PopoverArrow | Popover.Arrow | - |
PopoverCloseButton | Popover.CloseTrigger | Name change |
PopoverAnchor | Popover.Anchor | - |
When using trigger="hover" in v2, the codemod automatically transforms the
entire component to use HoverCard instead of Popover.
import {
Button,
Popover,
PopoverArrow,
PopoverBody,
PopoverContent,
PopoverTrigger,
} from "@chakra-ui/react"
;<Popover trigger="hover" openDelay={500} closeDelay={300}>
<PopoverTrigger>
<Button>Hover me</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverBody>This appears on hover</PopoverBody>
</PopoverContent>
</Popover>
import { Button, HoverCard } from "@chakra-ui/react"
;<HoverCard.Root openDelay={500} closeDelay={300}>
<HoverCard.Trigger asChild>
<Button>Hover me</Button>
</HoverCard.Trigger>
<HoverCard.Positioner>
<HoverCard.Content>
<HoverCard.Arrow />
<HoverCard.Body>This appears on hover</HoverCard.Body>
</HoverCard.Content>
</HoverCard.Positioner>
</HoverCard.Root>
Key Changes:
Popover namespace → HoverCard namespacetrigger="hover" prop is removed (implicit in HoverCard)openDelay and closeDelay props are preserved (specific to hover
interactions)Popover.* to HoverCard.*HoverCardBenefits:
Before (v2):
<Popover isOpen={isOpen} onClose={onClose}>
</Popover>
After (v3):
<Popover.Root
open={isOpen}
onOpenChange={(e) => {
if (!e.open) onClose()
}}
>
</Popover.Root>
Changes:
isOpen → opendefaultIsOpen → defaultOpenonClose + onOpen → onOpenChange with { open: boolean } parameterBefore (v2):
<Popover closeOnBlur={false}></Popover>
After (v3):
<Popover.Root closeOnInteractOutside={false}></Popover.Root>
Before (v2):
<Popover closeOnEsc={false}></Popover>
After (v3):
<Popover.Root closeOnEscape={false}></Popover.Root>
Before (v2):
<Popover isLazy></Popover>
After (v3):
<Popover.Root lazyMount></Popover.Root>
Before (v2):
<Popover isLazy lazyBehavior="unmount">
</Popover>
After (v3):
<Popover.Root lazyMount unmountOnExit>
</Popover.Root>
Note: lazyBehavior='keepMounted' is the default behavior in v3, so this
prop can be removed.
positioning object)All positioning-related props are now grouped in a positioning object:
Before (v2):
<Popover placement="top-start"></Popover>
After (v3):
<Popover.Root positioning={{ placement: "top-start" }}>
</Popover.Root>
Before (v2):
<Popover placement="bottom" gutter={8} flip={false} matchWidth strategy="fixed">
</Popover>
After (v3):
<Popover.Root
positioning={{
placement: "bottom",
gutter: 8,
flip: false,
sameWidth: true,
strategy: "fixed",
}}
>
</Popover.Root>
| v2 Prop | v3 Positioning Prop | Description |
|---|---|---|
placement | positioning.placement | Preferred placement |
boundary | positioning.boundary | Boundary element |
flip | positioning.flip | Enable flip behavior |
gutter | positioning.gutter | Gap between trigger and content |
matchWidth | positioning.sameWidth | Match trigger width |
offset | positioning.offset | Offset from trigger |
strategy | positioning.strategy | Positioning strategy |
arrowPadding | positioning.arrowPadding | Arrow padding |
preventOverflow | positioning.preventOverflow | Prevent overflow behavior |
The following props are passed through unchanged:
| Prop | Description |
|---|---|
autoFocus | Controls automatic focus behavior |
direction | Text direction (ltr/rtl) |
id | HTML id attribute |
Example:
// Before (v2)
<Popover autoFocus={false} direction='rtl' id='my-popover'>
</Popover>
// After (v3)
<Popover.Root autoFocus={false} direction='rtl' id='my-popover'>
</Popover.Root>
The following props have been removed:
| Removed Prop | Reason / Alternative |
|---|---|
computePositionOnMount | No longer needed |
returnFocusOnClose | Handled automatically |
arrowShadowColor | Use CSS styling instead |
trigger | Automatically transformed to HoverCard if value is "hover" |
arrowSize | Automatically transferred to Popover.Arrow with CSS variable |
modifiers | No longer supported - use positioning props instead |
openDelay | Removed for Popover, preserved for HoverCard (auto-detected) |
closeDelay | Removed for Popover, preserved for HoverCard (auto-detected) |
For accessing popover state within the render tree, use Popover.Context:
Before (v2):
<Popover>
{({ isOpen, onClose }) => (
<>
<PopoverTrigger>
<Button>Click to {isOpen ? "close" : "open"}</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>
<Button onClick={onClose}>Close</Button>
</PopoverBody>
</PopoverContent>
</>
)}
</Popover>
After (v3):
<Popover.Root>
<Popover.Context>
{({ open: isOpen, setOpen }) => {
const onClose = () => setOpen(false)
const onOpen = () => setOpen(true)
return (
<>
<Popover.Trigger asChild>
<Button>Click to {isOpen ? "close" : "open"}</Button>
</Popover.Trigger>
<Popover.Positioner>
<Popover.Content>
<Popover.Body>
<Button onClick={onClose}>Close</Button>
</Popover.Body>
</Popover.Content>
</Popover.Positioner>
</>
)
}}
</Popover.Context>
</Popover.Root>
Context API Changes:
isOpen → open: isOpen (destructuring rename)onClose → const onClose = () => setOpen(false) (helper function)onOpen → const onOpen = () => setOpen(true) (helper function)Only isOpen used:
// Before
{
;({ isOpen }) => <Button>{isOpen ? "Open" : "Closed"}</Button>
}
// After
{
;({ open: isOpen }) => <Button>{isOpen ? "Open" : "Closed"}</Button>
}
isOpen and onClose used:
// Before
{
;({ isOpen, onClose }) => (
<Button onClick={onClose}>{isOpen ? "Close" : "Open"}</Button>
)
}
// After
{
;({ open: isOpen, setOpen }) => {
const onClose = () => setOpen(false)
return <Button onClick={onClose}>{isOpen ? "Close" : "Open"}</Button>
}
}
All three used:
// Before
{
;({ isOpen, onClose, onOpen }) => (
<Button onClick={isOpen ? onClose : onOpen}>Toggle</Button>
)
}
// After
{
;({ open: isOpen, setOpen }) => {
const onClose = () => setOpen(false)
const onOpen = () => setOpen(true)
return <Button onClick={isOpen ? onClose : onOpen}>Toggle</Button>
}
}
Before (v2):
import {
Button,
Popover,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverFooter,
PopoverHeader,
PopoverTrigger,
} from "@chakra-ui/react"
export default function App() {
return (
<Popover>
<PopoverTrigger>
<Button>Trigger</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverArrow />
<PopoverHeader>Header</PopoverHeader>
<PopoverCloseButton />
<PopoverBody>Body content</PopoverBody>
<PopoverFooter>Footer content</PopoverFooter>
</PopoverContent>
</Popover>
)
}
After (v3):
import { Button, Popover } from "@chakra-ui/react"
export default function App() {
return (
<Popover.Root>
<Popover.Trigger asChild>
<Button>Trigger</Button>
</Popover.Trigger>
<Popover.Positioner>
<Popover.Content>
<Popover.Arrow />
<Popover.Title>Header</Popover.Title>
<Popover.CloseTrigger />
<Popover.Body>Body content</Popover.Body>
<Popover.Footer>Footer content</Popover.Footer>
</Popover.Content>
</Popover.Positioner>
</Popover.Root>
)
}
Before (v2):
import {
Button,
Popover,
PopoverContent,
PopoverTrigger,
} from "@chakra-ui/react"
function ControlledUsage() {
const { isOpen, onToggle, onClose } = useDisclosure()
return (
<Popover
isOpen={isOpen}
onClose={onClose}
placement="right"
closeOnBlur={false}
>
<PopoverTrigger>
<Button onClick={onToggle}>Popover Target</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverBody>Content</PopoverBody>
</PopoverContent>
</Popover>
)
}
After (v3):
import { Button, Popover } from "@chakra-ui/react"
import { useState } from "react"
function ControlledUsage() {
const [open, setOpen] = useState(false)
return (
<Popover.Root
open={open}
onOpenChange={(e) => setOpen(e.open)}
positioning={{ placement: "right" }}
closeOnInteractOutside={false}
>
<Popover.Trigger asChild>
<Button onClick={() => setOpen(!open)}>Popover Target</Button>
</Popover.Trigger>
<Popover.Positioner>
<Popover.Content>
<Popover.Body>Content</Popover.Body>
</Popover.Content>
</Popover.Positioner>
</Popover.Root>
)
}
Before (v2):
import {
Button,
Popover,
PopoverContent,
PopoverTrigger,
Portal,
} from "@chakra-ui/react"
export default function App() {
return (
<Popover>
<PopoverTrigger>
<Button>Trigger</Button>
</PopoverTrigger>
<Portal>
<PopoverContent>
<PopoverBody>Content</PopoverBody>
</PopoverContent>
</Portal>
</Popover>
)
}
After (v3):
import { Button, Popover, Portal } from "@chakra-ui/react"
export default function App() {
return (
<Popover.Root>
<Popover.Trigger asChild>
<Button>Trigger</Button>
</Popover.Trigger>
<Portal>
<Popover.Positioner>
<Popover.Content>
<Popover.Body>Content</Popover.Body>
</Popover.Content>
</Popover.Positioner>
</Portal>
</Popover.Root>
)
}
Note: Portal usage remains the same, but wraps Popover.Positioner instead
of PopoverContent.
Before (v2):
import {
Popover,
PopoverAnchor,
PopoverArrow,
PopoverBody,
PopoverCloseButton,
PopoverContent,
PopoverFooter,
PopoverHeader,
PopoverTrigger,
} from "@chakra-ui/react"
After (v3):
import { Popover } from "@chakra-ui/react"
All subcomponents are accessed via the compound component pattern:
Popover.Root, Popover.Trigger, etc.
After running the codemod, review your code for:
onClose/onOpen handlers on Root - Merge into onOpenChange:
// Before
onOpen={handleOpen} onClose={handleClose}
// After
onOpenChange={(e) => e.open ? handleOpen() : handleClose()}
Note: The codemod automatically handles render prop transformations - it
adds helper functions for onClose and onOpen when used in render props.
initialFocusRef - Automatically converted to function:
// Before
initialFocusRef={ref}
// After
initialFocusEl={() => ref.current}
Note: The codemod automatically wraps the ref in a function that returns
ref.current.
Hover trigger - Automatically transformed to HoverCard:
// The codemod automatically detects trigger="hover" and converts
// the entire component from Popover.* to HoverCard.*
// v2: <Popover trigger="hover">...</Popover>
// v3: <HoverCard.Root>...</HoverCard.Root>
import { HoverCard } from "@chakra-ui/react"
The transformation is complete and automatic - all subcomponents, props, and imports are updated.
After migration, test:
Solution: Ensure Popover.Content is wrapped in Popover.Positioner:
<Popover.Root>
<Popover.Trigger asChild>...</Popover.Trigger>
<Popover.Positioner>
{" "}
<Popover.Content>...</Popover.Content>
</Popover.Positioner>
</Popover.Root>
Solution: Update property names:
isOpen → popover.openonClose → popover.setOpen(false)onOpen → popover.setOpen(true)Solution: Add asChild prop to Popover.Trigger:
<Popover.Trigger asChild>
<Button>Click me</Button>
</Popover.Trigger>
If you encounter issues during migration: