tetra-ui Logotetra ui

Menu

A composable native menu built on Expo UI, with support for groups, separators, and nested submenus.

menu

Installation

npx shadcn@latest add @tetra-ui/menu

Usage

Compose a menu from a trigger and a content slot. Each item is built from MenuItem, MenuItemIcon, and MenuItemLabel parts.

import { Platform } from "react-native";
import { Button } from "@/components/ui/button";
import {
  Menu,
  MenuContent,
  MenuItem,
  MenuItemIcon,
  MenuItemLabel,
  MenuSeparator,
  MenuTrigger,
} from "@/components/ui/menu";
<Menu>
  <MenuTrigger>
    <Button variant="outline">Open</Button>
  </MenuTrigger>

  <MenuContent>
    <MenuItem onPress={() => {}}>
      <MenuItemIcon
        icon={Platform.select({
          ios: "gear",
          android: require("@expo/material-symbols/settings.xml"),
        })}
      />
      <MenuItemLabel>Settings</MenuItemLabel>
    </MenuItem>
    <MenuSeparator />
    <MenuItem onPress={() => {}} variant="destructive">
      <MenuItemIcon
        icon={Platform.select({
          ios: "trash",
          android: require("@expo/material-symbols/delete.xml"),
        })}
      />
      <MenuItemLabel>Delete</MenuItemLabel>
    </MenuItem>
  </MenuContent>
</Menu>

Icons

On iOS, pass an SF Symbol string to MenuItemIcon. On Android, pass a Material Symbol XML asset from @expo/material-symbols.

Use Platform.select for a cross-platform definition:

import { Platform } from "react-native";

<MenuItemIcon
  icon={Platform.select({
    ios: "person",
    android: require("@expo/material-symbols/person.xml"),
  })}
/>

Bare SF Symbol strings (for example icon="person") do not render on Android.

Disabled

Pass disabled to prevent interaction:

<MenuItem disabled>
  <MenuItemIcon
    icon={Platform.select({
      ios: "archivebox",
      android: require("@expo/material-symbols/archive.xml"),
    })}
  />
  <MenuItemLabel>Archive</MenuItemLabel>
</MenuItem>

Selected

Pass selected to render a checkmark on selectable items. Omit selected for regular action items. You control the state and update it in onPress.

Single-select (for example, sort options):

import { useState } from "react";

const [sort, setSort] = useState<"name" | "date">("name");

<MenuItem selected={sort === "name"} onPress={() => setSort("name")}>
  <MenuItemIcon
    icon={Platform.select({
      ios: "textformat",
      android: require("@expo/material-symbols/sort_by_alpha.xml"),
    })}
  />
  <MenuItemLabel>Name</MenuItemLabel>
</MenuItem>
<MenuItem selected={sort === "date"} onPress={() => setSort("date")}>
  <MenuItemIcon
    icon={Platform.select({
      ios: "calendar",
      android: require("@expo/material-symbols/calendar_today.xml"),
    })}
  />
  <MenuItemLabel>Date</MenuItemLabel>
</MenuItem>

Multi-toggle (for example, filters):

const [showCompleted, setShowCompleted] = useState(true);

<MenuItem
  selected={showCompleted}
  onPress={() => setShowCompleted((value) => !value)}
>
  <MenuItemIcon
    icon={Platform.select({
      ios: "checkmark.circle",
      android: require("@expo/material-symbols/check_circle.xml"),
    })}
  />
  <MenuItemLabel>Show completed</MenuItemLabel>
</MenuItem>

On iOS, selectable items use SwiftUI Toggle and render a trailing checkmark when selected is true. On Android, a trailing is shown. Destructive items with selected do not show a checkmark on iOS because Toggle has no destructive role.

Groups

Group related items with MenuGroup. A vertical group with a title renders as a section. A horizontal group lays icon-only items in a row.

<MenuContent>
  <MenuGroup title="My Account">
    <MenuItem onPress={() => {}}>
      <MenuItemIcon
        icon={Platform.select({
          ios: "person",
          android: require("@expo/material-symbols/person.xml"),
        })}
      />
      <MenuItemLabel>Profile</MenuItemLabel>
    </MenuItem>
  </MenuGroup>

  <MenuSeparator />

  <MenuGroup direction="horizontal" title="Quick Actions">
    <MenuItem onPress={() => {}}>
      <MenuItemIcon
        icon={Platform.select({
          ios: "plus",
          android: require("@expo/material-symbols/add.xml"),
        })}
      />
    </MenuItem>
    <MenuItem onPress={() => {}}>
      <MenuItemIcon
        icon={Platform.select({
          ios: "magnifyingglass",
          android: require("@expo/material-symbols/search.xml"),
        })}
      />
    </MenuItem>
  </MenuGroup>
</MenuContent>

On Android, horizontal groups use a compact row layout. Icon-only items work best in horizontal groups.

Nest a submenu with MenuSub, MenuSubTrigger, and MenuSubContent:

<MenuContent>
  <MenuSub>
    <MenuSubTrigger>
      <MenuItem>
        <MenuItemIcon
          icon={Platform.select({
            ios: "2.circle",
            android: require("@expo/material-symbols/counter_2.xml"),
          })}
        />
        <MenuItemLabel>More</MenuItemLabel>
      </MenuItem>
    </MenuSubTrigger>
    <MenuSubContent>
      <MenuItem onPress={() => {}}>
        <MenuItemIcon
          icon={Platform.select({
            ios: "doc",
            android: require("@expo/material-symbols/description.xml"),
          })}
        />
        <MenuItemLabel>Sub Item</MenuItemLabel>
      </MenuItem>
    </MenuSubContent>
  </MenuSub>
</MenuContent>

Platform behavior

PlatformNative primitive
iOSSwiftUI Menu
AndroidJetpack Compose DropdownMenu
WebNo-op stubs

Menus use native Expo UI primitives under the hood. Selecting a regular item dismisses the menu automatically on iOS and Android. Selectable items on iOS may keep the menu open after tap (native SwiftUI Toggle behavior); on Android, selectable items still dismiss the menu.

API Reference

Native surface on Expo UI (Menu / DropdownMenu). Documented props are the tetra-ui API; underlying Expo primitives are managed internally.

Root menu. Requires MenuTrigger and MenuContent children.

Single React element rendered as the menu label / anchor (typically a pressable).

Prop

Type

Menu items, groups, separators, and submenus.

Groups items. Vertical uses a section; horizontal uses a control group / row.

Prop

Type

A tappable row. When selected is defined, iOS uses a native toggle; Android shows a trailing checkmark.

Prop

Type

Label text for the item.

Prop

Type

Leading icon via Expo UI Icon name (SF Symbol on iOS; drawable / material symbol on Android — prefer platform-selected names for cross-platform).

Prop

Type

Native divider between items. No custom props.

Nested submenu. Same child contract as Menu (MenuSubTrigger + MenuSubContent).

Same shape as MenuTrigger for the submenu entry.

Same shape as MenuContent for nested items.

Changelog

2026-07-02 - Add support for selected items

  • Add optional selected prop to MenuItem for checkmark states.
  • iOS uses SwiftUI Toggle; Android renders a trailing checkmark.

2026-06-08 - Add support for disabled items & remove MenuItemIcon.select

  • Add support for disabled items.
  • Remove MenuItemIcon.select as it is no longer needed. Use Platform.select instead.

On this page