packages/codemod/docs/MENU_MIGRATION.md
This guide covers the migration of Chakra UI v2 Menu components to the v3 compound component API.
In v3, Menu has been redesigned with a compound component pattern that provides better composition and clearer component relationships. The codemod automatically handles most transformations.
| v2 Component | v3 Component |
|---|---|
<Menu> | <Menu.Root> |
<MenuButton> | <Menu.Trigger> (or with as prop: <Menu.Trigger asChild>) |
<MenuList> | <Portal> + <Menu.Positioner> + <Menu.Content> |
<MenuItem> | <Menu.Item> |
<MenuGroup> | <Menu.ItemGroup> + <Menu.ItemGroupLabel> |
<MenuDivider> | <Menu.Separator> |
<MenuOptionGroup> (radio) | <Menu.RadioItemGroup> + <Menu.RadioItem> |
<MenuOptionGroup> (checkbox) | <Menu.ItemGroup> + <Menu.CheckboxItem> |
as prop)v2:
import { Menu, MenuButton, MenuItem, MenuList } from "@chakra-ui/react"
;<Menu>
<MenuButton>Actions</MenuButton>
<MenuList>
<MenuItem>Download</MenuItem>
<MenuItem>Create a Copy</MenuItem>
<MenuItem>Delete</MenuItem>
</MenuList>
</Menu>
v3:
import { Menu, Portal } from "@chakra-ui/react"
;<Menu.Root>
<Menu.Trigger>Actions</Menu.Trigger>
<Portal>
<Menu.Positioner>
<Menu.Content>
<Menu.Item value="item-0">Download</Menu.Item>
<Menu.Item value="item-1">Create a Copy</Menu.Item>
<Menu.Item value="item-2">Delete</Menu.Item>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>
Note: When MenuButton has no as prop, it directly becomes
<Menu.Trigger>.
as propv2:
import { Button, Menu, MenuButton, MenuItem, MenuList } from "@chakra-ui/react"
;<Menu>
<MenuButton as={Button}>Actions</MenuButton>
<MenuList>
<MenuItem>Download</MenuItem>
</MenuList>
</Menu>
v3:
import { Button, Menu, Portal } from "@chakra-ui/react"
;<Menu.Root>
<Menu.Trigger asChild>
<Button>Actions</Button>
</Menu.Trigger>
<Portal>
<Menu.Positioner>
<Menu.Content>
<Menu.Item value="item-0">Download</Menu.Item>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>
Note: When MenuButton has an as prop, it becomes <Menu.Trigger asChild>
wrapping the component specified in as.
v2:
import { ChevronDownIcon } from "@chakra-ui/icons"
import { Menu, MenuButton, MenuItem, MenuList } from "@chakra-ui/react"
;<Menu>
<MenuButton rightIcon={<ChevronDownIcon />}>Actions</MenuButton>
<MenuList>
<MenuItem>Download</MenuItem>
</MenuList>
</Menu>
v3:
import { ChevronDownIcon } from "@chakra-ui/icons"
import { Menu, Portal } from "@chakra-ui/react"
;<Menu.Root>
<Menu.Trigger asChild>
<Button>
Actions
<ChevronDownIcon />
</Button>
</Menu.Trigger>
<Portal>
<Menu.Positioner>
<Menu.Content>
<Menu.Item value="item-0">Download</Menu.Item>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>
v2:
import {
Menu,
MenuButton,
MenuDivider,
MenuGroup,
MenuItem,
MenuList,
} from "@chakra-ui/react"
;<Menu>
<MenuButton>Profile</MenuButton>
<MenuList>
<MenuGroup title="Profile">
<MenuItem>My Account</MenuItem>
<MenuItem>Payments</MenuItem>
</MenuGroup>
<MenuDivider />
<MenuGroup title="Help">
<MenuItem>Docs</MenuItem>
<MenuItem>FAQ</MenuItem>
</MenuGroup>
</MenuList>
</Menu>
v3:
import { Menu, Portal } from "@chakra-ui/react"
;<Menu.Root>
<Menu.Trigger asChild>
<Button>Profile</Button>
</Menu.Trigger>
<Portal>
<Menu.Positioner>
<Menu.Content>
<Menu.ItemGroup>
<Menu.ItemGroupLabel>Profile</Menu.ItemGroupLabel>
<Menu.Item value="item-0">My Account</Menu.Item>
<Menu.Item value="item-1">Payments</Menu.Item>
</Menu.ItemGroup>
<Menu.Separator />
<Menu.ItemGroup>
<Menu.ItemGroupLabel>Help</Menu.ItemGroupLabel>
<Menu.Item value="item-2">Docs</Menu.Item>
<Menu.Item value="item-3">FAQ</Menu.Item>
</Menu.ItemGroup>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>
| v2 Prop | v3 Prop | Notes |
|---|---|---|
isLazy | lazyMount + unmountOnExit | Splits into two props |
placement | positioning.placement | Grouped into positioning object |
gutter | positioning.gutter | Grouped into positioning object |
offset | positioning.offset | Grouped into positioning object |
flip | positioning.flip | Grouped into positioning object |
strategy | positioning.strategy | Grouped into positioning object |
boundary | positioning.boundary | Wrapped in arrow function |
closeOnSelect | closeOnSelect | Unchanged |
closeOnBlur | closeOnInteractOutside | Direct rename |
| v2 Prop | v3 Prop | Notes |
|---|---|---|
onClick | onSelect | Renamed |
| N/A | value | Required in v3 - auto-generated by codemod |
v2:
<MenuItem onClick={() => console.log("Download")}>Download</MenuItem>
v3:
<Menu.Item value="item-0" onSelect={() => console.log("Download")}>
Download
</Menu.Item>
v2:
<Menu isLazy></Menu>
v3:
<Menu.Root lazyMount unmountOnExit>
</Menu.Root>
All positioning-related props are grouped into a single positioning object:
v2:
<Menu placement="right-end" gutter={8} offset={[0, 10]} flip={false}>
</Menu>
v3:
<Menu.Root
positioning={{
placement: "right-end",
gutter: 8,
offset: [0, 10],
flip: false,
}}
>
</Menu.Root>
In v3, MenuOptionGroup is split into type-specific patterns:
v2:
<MenuOptionGroup defaultValue="asc" title="Order" type="radio">
<MenuItemOption value="asc">Ascending</MenuItemOption>
<MenuItemOption value="desc">Descending</MenuItemOption>
</MenuOptionGroup>
v3:
<Menu.RadioItemGroup defaultValue="asc">
<Menu.RadioItem value="asc">Ascending</Menu.RadioItem>
<Menu.RadioItem value="desc">Descending</Menu.RadioItem>
</Menu.RadioItemGroup>
Note: The title attribute is removed. The codemod preserves it on the
group, but you'll need to manually manage state with value and onValueChange
props.
v2:
<MenuOptionGroup title="Notifications" type="checkbox">
<MenuItemOption value="email">Email</MenuItemOption>
<MenuItemOption value="phone">Phone</MenuItemOption>
</MenuOptionGroup>
v3:
<Menu.ItemGroup>
<Menu.CheckboxItem value="email">Email</Menu.CheckboxItem>
<Menu.CheckboxItem value="phone">Phone</Menu.CheckboxItem>
</Menu.ItemGroup>
Important: Unlike radio groups, there is NO Menu.CheckboxItemGroup in v3.
Checkbox items use regular Menu.ItemGroup, and you'll need to manually add
state management using the useCheckboxGroup hook or custom state.
In v3, MenuList is replaced with a Portal wrapper that contains the
positioning logic:
Structure:
<Portal>
<Menu.Positioner>
<Menu.Content></Menu.Content>
</Menu.Positioner>
</Portal>
The codemod automatically:
Portal to imports from @chakra-ui/reactimport { ChevronDownIcon } from "@chakra-ui/icons"
import {
Button,
Menu,
MenuButton,
MenuDivider,
MenuGroup,
MenuItem,
MenuList,
} from "@chakra-ui/react"
function MenuExample() {
const handleDownload = () => console.log("Download")
return (
<Menu placement="right-end" isLazy>
<MenuButton as={Button} rightIcon={<ChevronDownIcon />}>
Actions
</MenuButton>
<MenuList>
<MenuItem onClick={handleDownload}>Download</MenuItem>
<MenuItem>Create a Copy</MenuItem>
<MenuDivider />
<MenuGroup title="Help">
<MenuItem>Docs</MenuItem>
<MenuItem>FAQ</MenuItem>
</MenuGroup>
</MenuList>
</Menu>
)
}
import { ChevronDownIcon } from "@chakra-ui/icons"
import { Button, Menu, Portal } from "@chakra-ui/react"
function MenuExample() {
const handleDownload = () => console.log("Download")
return (
<Menu.Root positioning={{ placement: "right-end" }} lazyMount unmountOnExit>
<Menu.Trigger asChild>
<Button>
Actions
<ChevronDownIcon />
</Button>
</Menu.Trigger>
<Portal>
<Menu.Positioner>
<Menu.Content>
<Menu.Item value="item-0" onSelect={handleDownload}>
Download
</Menu.Item>
<Menu.Item value="item-1">Create a Copy</Menu.Item>
<Menu.Separator />
<Menu.ItemGroup>
<Menu.ItemGroupLabel>Help</Menu.ItemGroupLabel>
<Menu.Item value="item-2">Docs</Menu.Item>
<Menu.Item value="item-3">FAQ</Menu.Item>
</Menu.ItemGroup>
</Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>
)
}
Note: Since the v2 example uses as={Button}, the v3 version uses
<Menu.Trigger asChild><Button>...</Button></Menu.Trigger>. If the v2 example
had just <MenuButton>, it would become <Menu.Trigger>...</Menu.Trigger>
without the Button wrapper.
The codemod consolidates imports:
v2:
import {
Menu,
MenuButton,
MenuDivider,
MenuGroup,
MenuItem,
MenuItemOption,
MenuList,
MenuOptionGroup,
} from "@chakra-ui/react"
v3:
import { Menu, Portal } from "@chakra-ui/react"
Note: Button import is not added automatically - ensure it's imported if
using as={Button}.
npx @chakra-ui/codemod@latest --transform menu src/**/*.tsx
After running the codemod, review:
Auto-generated value props: Menu.Item gets value="item-0",
value="item-1", etc. - consider using semantic values:
// Generated
<Menu.Item value="item-0">Account</Menu.Item>
// Better - semantic values
<Menu.Item value="account">Account</Menu.Item>
Button/Component imports:
as prop, it becomes <Menu.Trigger> directly - no
additional imports neededas={Button} or as={IconButton}, ensure that component
is imported:import { Menu, Portal, Button } from '@chakra-ui/react'
// or
import { Menu, Portal, IconButton } from '@chakra-ui/react'
MenuGroup title: The title attribute is converted to
Menu.ItemGroupLabel as the first child - verify text content.
onClick callbacks: Update to use onSelect and ensure correct
signatures.
Positioning props: All positioning is grouped into positioning object
on Menu.Root.
MenuButton with as prop: If using as={IconButton}, ensure
IconButton is imported.
Checkbox/Radio state management: The codemod transforms the structure but does NOT add state management:
value and onValueChange props with
useStateuseCheckboxGroup hook or custom state
with checked and onCheckedChange propsSee examples in the v3 docs for proper state management patterns.
Problem: Menu items don't respond to clicks.
Solution: Ensure onSelect is used instead of onClick:
// ✅ Correct
<Menu.Item value="download" onSelect={() => console.log('Download')}>
// ❌ Wrong
<Menu.Item value="download" onClick={() => console.log('Download')}>
Problem: Menu doesn't open when clicking the trigger.
Solution: Ensure the structure includes Portal > Menu.Positioner > Menu.Content:
<Menu.Root>
<Menu.Trigger asChild>
<Button>Actions</Button>
</Menu.Trigger>
<Portal>
<Menu.Positioner>
<Menu.Content></Menu.Content>
</Menu.Positioner>
</Portal>
</Menu.Root>
Problem: TypeScript complains about missing value prop on Menu.Item.
Solution: In v3, value is required on Menu.Item. The codemod
auto-generates values, but you may need to update them manually.
Problem: Runtime error that Button is not defined.
Solution: Add Button to your imports:
import { Button, Menu, Portal } from "@chakra-ui/react"
Problem: Menu appears in wrong position after migration.
Solution: Check that positioning props are correctly grouped:
// ✅ Correct
<Menu.Root positioning={{ placement: 'right-end', gutter: 8 }}>
// ❌ Wrong
<Menu.Root placement="right-end" gutter={8}>