Menu
A selectable list in a popover with nested submenus
- Full keyboard navigation.
- Submenus, items, icons, images, checkboxes, groups, and more.
- Modal and non-modal modes.
- Native menus on native platforms. Menu displays a list of actions or options in a floating panel triggered by a button. It supports nested submenus, keyboard navigation, native platform menus, and automatically stacks above other content.
Installation
Menu is already installed in @hanzo/gui, or you can install it independently:
yarn add @hanzogui/menuIf you want to use native menus, add these dependencies:
yarn add @react-native-menu/menu
yarn add react-native-ios-context-menu
yarn add react-native-ios-utilities
yarn add zeego
yarn add sf-symbols-typescriptThen add the setup import at your app entry point:
import '@hanzogui/native/setup-zeego'Expo Router users: This import must run before expo-router/entry. Create an index.js at your project root that imports the setup first, then expo-router, and update your package.json main field to "index.js". See the upgrade guide for details.
Anatomy
Import all parts and piece them together.
import { Menu } from '@hanzo/gui' // or '@hanzogui/menu'
export default () => (
<Menu>
<Menu.Trigger asChild>
<Button />
</Menu.Trigger>
<Menu.Portal zIndex={100}>
<Menu.Content>
<Menu.Item>
<Menu.ItemTitle>About Notes</Menu.ItemTitle>
</Menu.Item>
<Menu.Item>
<Menu.ItemTitle>Settings</Menu.ItemTitle>
</Menu.Item>
{/* when title is nested inside a React element then you need to use `textValue` */}
<Menu.Item textValue="Calendar">
<Menu.ItemTitle>
<Text>Calendar</Text>
</Menu.ItemTitle>
<Menu.ItemIcon>
<Calendar color="gray" size="$1" />
</Menu.ItemIcon>
</Menu.Item>
<Menu.Separator />
<Menu.Sub>
<Menu.SubTrigger>
<Menu.ItemTitle>Actions</Menu.ItemTitle>
</Menu.SubTrigger>
<Menu.Portal zIndex={200}>
<Menu.SubContent>
<Menu.Label fontSize={'$1'}>Note settings</Menu.Label>
<Menu.Item onSelect={onSelect} key="create-note">
<Menu.ItemTitle>Create note</Menu.ItemTitle>
</Menu.Item>
<Menu.Item onSelect={onSelect} key="delete-all">
<Menu.ItemTitle>Delete all notes</Menu.ItemTitle>
</Menu.Item>
<Menu.Item onSelect={onSelect} key="sync-all">
<Menu.ItemTitle>Sync notes</Menu.ItemTitle>
</Menu.Item>
</Menu.SubContent>
</Menu.Portal>
</Menu.Sub>
</Menu.Content>
</Menu.Portal>
</Menu>
)API Reference
Menu
Contains every component for the Menu.
| Prop | Type | Default | Required |
|---|---|---|---|
| children | React.ReactNode | - | ✓ |
| placement | Placement | - | - |
| open | boolean | - | - |
| defaultOpen | boolean | - | - |
| onOpenChange | (open: boolean) => void | - | - |
| onOpenWillChange | (open: boolean) => void | - | - |
| modal | boolean | true | - |
| stayInFrame | ShiftProps | boolean | { padding: 10 } | - |
| allowFlip | FlipProps | boolean | - | - |
| offset | OffsetOptions | 10 | - |
| resize | boolean | true | - |
| unstyled | boolean | - | - |
Menu.Portal
Required for rendering the menu content.
| Prop | Type | Default | Required |
|---|---|---|---|
| zIndex | number | - | - |
| children | React.ReactNode | - | ✓ |
| forceMount | true | - | - |
Menu.Trigger
The menu will only be triggered when the user right-clicks or long-presses within the trigger area.
| Prop | Type | Default | Required |
|---|---|---|---|
| action | press|longPress | longPress | - |
Menu.Content
Contains the content of the menu.
| Prop | Type | Default | Required |
|---|---|---|---|
| children | React.ReactNode | - | ✓ |
| loop | boolean | false | - |
| forceMount | true | - | - |
| onCloseAutoFocus | (event: Event) => void | - | - |
| onEscapeKeyDown | (event: KeyboardEvent) => void | - | - |
| onPointerDownOutside | (event: PointerEvent) => void | - | - |
| onInteractOutside | (event: Event) => void | - | - |
Menu.Item
A selectable menu item that triggers an action when selected.
| Prop | Type | Default | Required |
|---|---|---|---|
| key | string | - | ✓ |
| disabled | boolean | false | - |
| destructive | boolean | - | - |
| hidden | boolean | - | - |
| onSelect | (event?: Event) => void | - | - |
| onFocus | () => void | - | - |
| onBlur | () => void | - | - |
| textValue | string | - | - |
Menu.ItemTitle
Renders the title of the menu item.
| Prop | Type | Default | Required |
|---|---|---|---|
| children | string | React.ReactNode | - | ✓ |
You can directly pass a text node to the ItemTitle. However, if you use a nested
React node like <Text>, you need to pass textValue to the <Item> so that it
works with native menus.
Menu.ItemIcon
A component to render an icon. For non-native menus, you can pass an icon
component. For native menus, you can pass platform-specific icons to the
android and ios props.
On iOS, it renders the native SF Symbols icons.
| Prop | Type | Default | Required |
|---|---|---|---|
| children | React.ReactNode | - | - |
| ios | object | - | - |
| android | object | - | - |
<Menu.ItemIcon
ios={{
name: '0.circle.fill', // required
pointSize: 5,
weight: 'semibold',
scale: 'medium',
// can also be a color string. Requires iOS 15+
hierarchicalColor: {
dark: 'blue',
light: 'green',
},
// alternative to hierarchical color. Requires iOS 15+
paletteColors: [
{
dark: 'blue',
light: 'green',
},
],
}}
>
<CircleIcon />
</Menu.ItemIcon>Menu.ItemImage
A component to render an item image. For native menus, it only works on iOS. It
takes the same props as @hanzogui/image.
Menu.ItemSubtitle
A component to render a subtitle for the menu item. For native menus, it only works on iOS.
| Prop | Type | Default | Required |
|---|---|---|---|
| children | string | - | ✓ |
Menu.Group
A component that groups multiple menu items together.
| Prop | Type | Default | Required |
|---|---|---|---|
| children | React.ReactNode | - | ✓ |
Menu.CheckboxItem
A menu item with a checkbox that can be toggled on and off.
| Prop | Type | Default | Required |
|---|---|---|---|
| key | string | - | ✓ |
| disabled | boolean | false | - |
| destructive | boolean | - | - |
| hidden | boolean | - | - |
| onFocus | () => void | - | - |
| onBlur | () => void | - | - |
| textValue | string | - | - |
| value | 'on' | 'off' | 'mixed' | - | - |
| onValueChange | (state, prevState) => void | - | - |
| checked | boolean | - | - |
| onCheckedChange | (checked: boolean) => void | - | - |
Menu.ItemIndicator
Use inside Menu.CheckboxItem or Menu.RadioItem to indicate when an item is checked.
This allows you to conditionally render a checkmark.
<Menu.ItemIndicator>
<CheckmarkIcon /> {/* This does not work with the native prop. */}
</Menu.ItemIndicator>| Prop | Type | Default | Required |
|---|---|---|---|
| children | React.ReactNode | - | - |
| forceMount | true | - | - |
Menu.Label
Renders a non-focusable label for a group of items. On native menus, only one label is supported per menu and submenu.
| Prop | Type | Default | Required |
|---|---|---|---|
| children | string | - | ✓ |
| textValue | string | - | - |
Menu.Arrow
Renders an arrow pointing to the trigger.
| Prop | Type | Default | Required |
|---|---|---|---|
| size | number | SizeToken | - | - |
| unstyled | boolean | - | - |
Menu.Separator
Renders a visual divider between menu items. Web only.
Menu.Sub
A container for nested submenu components.
| Prop | Type | Default | Required |
|---|---|---|---|
| children | React.ReactNode | - | ✓ |
| open | boolean | - | - |
| onOpenChange | (open: boolean) => void | - | - |
Menu.SubContent
Renders the content of a submenu. Same props as Menu.Content, excluding side and
align.
Menu.SubTrigger
A menu item that opens a submenu on hover or focus. Accepts the same props as
Menu.Item.
Menu.ScrollView
A scrollable container for menu items. Use this inside Menu.Content when you have many items that may overflow. The menu automatically constrains to available viewport space (via resize prop), and ScrollView handles the overflow. Scrollbars are hidden by default.
<Menu.Content>
<Menu.ScrollView>
{/* Many menu items */}
</Menu.ScrollView>
</Menu.Content>Styling
Item Highlight Behavior
Menu items use focusStyle for highlighting rather than hoverStyle. This ensures a unified highlight experience when switching between mouse and keyboard navigation - only one item is ever highlighted at a time.
When you hover over an item, it receives focus, which triggers the focusStyle. When you use arrow keys to navigate, focus moves to the new item, removing the highlight from the previous one.
// Default behavior - uses focusStyle for highlight
<Menu.Item>
<Menu.ItemTitle>Settings</Menu.ItemTitle>
</Menu.Item>
// Custom highlight styling - use focusStyle, not hoverStyle
<Menu.Item
focusStyle={{ backgroundColor: '$blue5' }}
>
<Menu.ItemTitle>Custom Highlight</Menu.ItemTitle>
</Menu.Item>Avoid using hoverStyle for background highlights on Menu.Item. This can cause "double
highlighting" when switching between mouse and keyboard - where both the hovered item
and the focused item appear highlighted simultaneously.
Last updated on