Hanzo GUI

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/menu

If 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-typescript

Then 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

Contains every component for the Menu.

PropTypeDefaultRequired
childrenReact.ReactNode-
placementPlacement--
openboolean--
defaultOpenboolean--
onOpenChange(open: boolean) => void--
onOpenWillChange(open: boolean) => void--
modalbooleantrue-
stayInFrameShiftProps | boolean{ padding: 10 }-
allowFlipFlipProps | boolean--
offsetOffsetOptions10-
resizebooleantrue-
unstyledboolean--

Required for rendering the menu content.

PropTypeDefaultRequired
zIndexnumber--
childrenReact.ReactNode-
forceMounttrue--

The menu will only be triggered when the user right-clicks or long-presses within the trigger area.

PropTypeDefaultRequired
actionpress|longPresslongPress-

Contains the content of the menu.

PropTypeDefaultRequired
childrenReact.ReactNode-
loopbooleanfalse-
forceMounttrue--
onCloseAutoFocus(event: Event) => void--
onEscapeKeyDown(event: KeyboardEvent) => void--
onPointerDownOutside(event: PointerEvent) => void--
onInteractOutside(event: Event) => void--

A selectable menu item that triggers an action when selected.

PropTypeDefaultRequired
keystring-
disabledbooleanfalse-
destructiveboolean--
hiddenboolean--
onSelect(event?: Event) => void--
onFocus() => void--
onBlur() => void--
textValuestring--

Renders the title of the menu item.

PropTypeDefaultRequired
childrenstring | 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.

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.

PropTypeDefaultRequired
childrenReact.ReactNode--
iosobject--
androidobject--
<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>

A component to render an item image. For native menus, it only works on iOS. It takes the same props as @hanzogui/image.

A component to render a subtitle for the menu item. For native menus, it only works on iOS.

PropTypeDefaultRequired
childrenstring-

A component that groups multiple menu items together.

PropTypeDefaultRequired
childrenReact.ReactNode-

A menu item with a checkbox that can be toggled on and off.

PropTypeDefaultRequired
keystring-
disabledbooleanfalse-
destructiveboolean--
hiddenboolean--
onFocus() => void--
onBlur() => void--
textValuestring--
value'on' | 'off' | 'mixed'--
onValueChange(state, prevState) => void--
checkedboolean--
onCheckedChange(checked: boolean) => void--

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>
PropTypeDefaultRequired
childrenReact.ReactNode--
forceMounttrue--

Renders a non-focusable label for a group of items. On native menus, only one label is supported per menu and submenu.

PropTypeDefaultRequired
childrenstring-
textValuestring--

Renders an arrow pointing to the trigger.

PropTypeDefaultRequired
sizenumber | SizeToken--
unstyledboolean--

Renders a visual divider between menu items. Web only.

A container for nested submenu components.

PropTypeDefaultRequired
childrenReact.ReactNode-
openboolean--
onOpenChange(open: boolean) => void--

Renders the content of a submenu. Same props as Menu.Content, excluding side and align.

A menu item that opens a submenu on hover or focus. Accepts the same props as Menu.Item.

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

On this page