Expo UI and Router for iOSExpo SDK 57 and later, iOS only
Components

Menu

A SwiftUI Menu component for displaying dropdown menus.

Expo UI Menu matches the official SwiftUI Menu API and supports styling via the 0 modifier.

  • Menu opens on a single tap.
  • For long-press interactions, use 1 instead.

Note: On tvOS, Menu requires tvOS 17.0 or later.

Usage

Simple text label

import { Host, Menu, Button } from '@expo/ui/swift-ui';

export default function SimpleMenuExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Menu label="Options">
        <Button
          label="Option 1"
          onPress={() => console.log('Option 1')}
        />
        <Button
          label="Option 2"
          onPress={() => console.log('Option 2')}
        />
        <Button
          label="Option 3"
          onPress={() => console.log('Option 3')}
        />
      </Menu>
    </Host>
  );
}
An open menu listing Option 1, Option 2, and Option 3
An open menu listing Option 1, Option 2, and Option 3

Text label with SF Symbol

import { Host, Menu, Button, Divider } from '@expo/ui/swift-ui';

export default function MenuWithIconExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Menu label="More" systemImage="ellipsis.circle">
        <Button
          label="Settings"
          systemImage="gear"
          onPress={() => console.log('Settings')}
        />
        <Button
          label="Profile"
          systemImage="person"
          onPress={() => console.log('Profile')}
        />
        <Divider />
        <Button
          label="Delete"
          role="destructive"
          systemImage="trash"
          onPress={() => console.log('Delete')}
        />
      </Menu>
    </Host>
  );
}
An open menu with Settings and Profile rows, a divider, and a red Delete row
An open menu with Settings and Profile rows, a divider, and a red Delete row

Custom label

You can pass a React node as the label for custom styling.

import { Host, Menu, Button, Text } from '@expo/ui/swift-ui';
import { foregroundStyle } from '@expo/ui/swift-ui/modifiers';

export default function CustomLabelMenuExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Menu
        label={
          <Text modifiers={[foregroundStyle('accentColor')]}>
            Custom Label
          </Text>
        }>
        <Button
          label="Action 1"
          onPress={() => console.log('Action 1')}
        />
        <Button
          label="Action 2"
          onPress={() => console.log('Action 2')}
        />
      </Menu>
    </Host>
  );
}
A menu trigger labeled Custom Label in the accent color
A menu trigger labeled Custom Label in the accent color

React Native components as label

You can use a React Native view (such as Pressable) as the menu's label by wrapping it in RNHostView.

import { Host, Menu, Button, RNHostView } from '@expo/ui/swift-ui';
import { Pressable, Text } from 'react-native';

export default function RNLabelMenuExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Menu
        label={
          <RNHostView matchContents>
            <Pressable
              onPress={() => console.log('RN trigger pressed')}
              style={{
                alignSelf: 'flex-start',
                paddingHorizontal: 16,
                paddingVertical: 10,
                borderRadius: 8,
                backgroundColor: '#9B59B6',
              }}>
              <Text style={{ color: 'white', fontWeight: '600' }}>
                RN Pressable Trigger
              </Text>
            </Pressable>
          </RNHostView>
        }>
        <Button
          label="Item 1"
          onPress={() => console.log('Item 1')}
        />
        <Button
          label="Item 2"
          onPress={() => console.log('Item 2')}
        />
      </Menu>
    </Host>
  );
}
A purple React Native Pressable used as the menu trigger
A purple React Native Pressable used as the menu trigger

Nested menu

Menus can be nested to create submenus.

import { Host, Menu, Button } from '@expo/ui/swift-ui';

export default function NestedMenuExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Menu label="Main Menu">
        <Button
          label="Item 1"
          onPress={() => console.log('Item 1')}
        />
        <Menu label="Submenu">
          <Button
            label="Sub Item 1"
            onPress={() => console.log('Sub Item 1')}
          />
          <Button
            label="Sub Item 2"
            onPress={() => console.log('Sub Item 2')}
          />
        </Menu>
        <Button
          label="Item 2"
          onPress={() => console.log('Item 2')}
        />
      </Menu>
    </Host>
  );
}
An open menu with Item 1, a Submenu row with a chevron, and Item 2
An open menu with Item 1, a Submenu row with a chevron, and Item 2

With primary action

When onPrimaryAction is provided, a single tap triggers the primary action while a long-press shows the menu.

import { Host, Menu, Button } from '@expo/ui/swift-ui';

export default function PrimaryActionMenuExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Menu
        label="Tap or hold"
        systemImage="play.circle"
        onPrimaryAction={() =>
          console.log('Primary action triggered!')
        }>
        <Button
          label="Menu Item 1"
          onPress={() => console.log('Menu Item 1')}
        />
        <Button
          label="Menu Item 2"
          onPress={() => console.log('Menu Item 2')}
        />
        <Button
          label="Menu Item 3"
          onPress={() => console.log('Menu Item 3')}
        />
      </Menu>
    </Host>
  );
}
A menu trigger labeled Tap or hold with a play icon
A menu trigger labeled Tap or hold with a play icon

Styling with modifiers

You can use the buttonStyle modifier to change the appearance of the menu trigger.

import { Host, Menu, Button } from '@expo/ui/swift-ui';
import { buttonStyle } from '@expo/ui/swift-ui/modifiers';

export default function StyledMenuExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Menu
        label="Styled Menu"
        modifiers={[buttonStyle('borderedProminent')]}>
        <Button
          label="Styled Action 1"
          onPress={() => console.log('Styled 1')}
        />
        <Button
          label="Styled Action 2"
          onPress={() => console.log('Styled 2')}
        />
      </Menu>
    </Host>
  );
}
A menu trigger rendered as a filled blue button
A menu trigger rendered as a filled blue button

Glass menu

To create a menu with the iOS Liquid Glass appearance, use buttonStyle('glass') or buttonStyle('glassProminent') on the Menu component.

Important: Do not apply the glassEffect() modifier to the Menu's label view to achieve a glass look. This causes a visual artifact where a rectangular halo briefly appears behind the trigger when the menu is dismissed. Always use buttonStyle instead, which correctly integrates with the Menu's dismiss animation.

import { Host, Menu, Button } from '@expo/ui/swift-ui';
import { buttonStyle } from '@expo/ui/swift-ui/modifiers';

export default function GlassMenuExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Menu
        label="Glass Menu"
        systemImage="ellipsis.circle"
        modifiers={[buttonStyle('glass')]}>
        <Button
          label="Action 1"
          onPress={() => console.log('Action 1')}
        />
        <Button
          label="Action 2"
          onPress={() => console.log('Action 2')}
        />
      </Menu>
    </Host>
  );
}
A menu trigger rendered as a glass capsule with an ellipsis icon
A menu trigger rendered as a glass capsule with an ellipsis icon

For a more prominent glass effect, use glassProminent:

import { Host, Menu, Button } from '@expo/ui/swift-ui';
import { buttonStyle } from '@expo/ui/swift-ui/modifiers';

export default function GlassProminentMenuExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Menu
        label="Glass Prominent Menu"
        systemImage="slider.horizontal.3"
        modifiers={[buttonStyle('glassProminent')]}>
        <Button
          label="Settings"
          systemImage="gear"
          onPress={() => console.log('Settings')}
        />
        <Button
          label="Filter"
          systemImage="line.3.horizontal.decrease"
          onPress={() => console.log('Filter')}
        />
      </Menu>
    </Host>
  );
}
A menu trigger rendered as a prominent blue glass capsule with a sliders icon
A menu trigger rendered as a prominent blue glass capsule with a sliders icon

With control group

Use a ControlGroup inside a menu to render a horizontal row of icon buttons, similar to the quick actions row in Apple Music or Safari menus.

import {
  Host,
  Menu,
  ControlGroup,
  Button,
  Section,
  Divider,
} from '@expo/ui/swift-ui';

export default function MenuWithControlGroupExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Menu label="Song Options" systemImage="ellipsis.circle">
        <ControlGroup>
          <Button
            systemImage="plus"
            label="Add"
            onPress={() => console.log('Add')}
          />
          <Button
            systemImage="star"
            label="Favorite"
            onPress={() => console.log('Favorite')}
          />
          <Button
            systemImage="square.and.arrow.up"
            label="Share"
            onPress={() => console.log('Share')}
          />
        </ControlGroup>
        <Section>
          <Button
            systemImage="text.badge.plus"
            label="Add to a playlist"
            onPress={() => console.log('Add to playlist')}
          />
          <Button
            systemImage="antenna.radiowaves.left.and.right"
            label="Create station"
            onPress={() => console.log('Create station')}
          />
        </Section>
        <Divider />
        <Button
          systemImage="hand.thumbsdown"
          label="Suggest less"
          onPress={() => console.log('Suggest less')}
        />
      </Menu>
    </Host>
  );
}
An open menu with an Add, Favorite, and Share row above two sections of list items
An open menu with an Add, Favorite, and Share row above two sections of list items

Disabled items

Use the disabled(true) modifier on a menu Button to render it greyed-out and non-interactive. The button still appears in the menu but won't fire onPress.

import { Host, Menu, Button } from '@expo/ui/swift-ui';
import { disabled } from '@expo/ui/swift-ui/modifiers';

export default function DisabledMenuItemExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Menu label="Options">
        <Button
          label="Available"
          onPress={() => console.log('Available')}
        />
        <Button
          label="Locked"
          systemImage="lock"
          modifiers={[disabled(true)]}
          onPress={() => console.log('This never fires')}
        />
      </Menu>
    </Host>
  );
}
An open menu with an Available row and a greyed-out Locked row
An open menu with an Available row and a greyed-out Locked row

Selectable items (checkmarks)

A SwiftUI Toggle placed inside a Menu automatically renders as a row with its SF Symbol (when systemImage is set) and a checkmark before that symbol when isOn is true. Use this pattern instead of inventing a custom checkmark item.

import { Host, Menu, Button, Toggle } from '@expo/ui/swift-ui';
import { useState } from 'react';

export default function CheckmarkMenuItemExample() {
  const [showCompleted, setShowCompleted] = useState(true);
  const [showArchived, setShowArchived] = useState(false);

  return (
    <Host style={{ flex: 1 }}>
      <Menu
        label="Filter"
        systemImage="line.3.horizontal.decrease.circle">
        <Toggle
          isOn={showCompleted}
          label="Show completed"
          systemImage="checkmark.circle"
          onIsOnChange={setShowCompleted}
        />
        <Toggle
          isOn={showArchived}
          label="Show archived"
          systemImage="archivebox"
          onIsOnChange={setShowArchived}
        />
        <Button
          label="Clear filters"
          onPress={() => console.log('Clear')}
        />
      </Menu>
    </Host>
  );
}
An open menu where Show completed has a checkmark and Show archived does not
An open menu where Show completed has a checkmark and Show archived does not

Icon only menu button

Use the labelStyle('iconOnly') modifier to display only the icon without the label text. The label prop should still be provided for accessibility purposes.

import { Host, Menu, Button } from '@expo/ui/swift-ui';
import { labelStyle } from '@expo/ui/swift-ui/modifiers';

export default function IconOnlyMenuExample() {
  return (
    <Host style={{ flex: 1 }}>
      <Menu
        label="More options"
        systemImage="ellipsis.circle"
        modifiers={[labelStyle('iconOnly')]}>
        <Button
          label="Menu Item 1"
          onPress={() => console.log('Menu Item 1')}
        />
        <Button
          label="Menu Item 2"
          onPress={() => console.log('Menu Item 2')}
        />
        <Button
          label="Menu Item 3"
          onPress={() => console.log('Menu Item 3')}
        />
      </Menu>
    </Host>
  );
}
A menu trigger showing only a circled ellipsis icon
A menu trigger showing only a circled ellipsis icon

API

import { Menu } from '@expo/ui/swift-ui';

Component

Type: React.Element<MenuProps>

Displays a dropdown menu when tapped.

Props for the Menu component.

MenuProps

children

Type: ReactNode

The menu's content items, which are shown when the menu is opened. Can contain Button, Toggle, Picker, Section, Divider or nested Menu components.

label

Type: ReactNode

The label for the menu trigger. Can be a string for simple text labels, or a ReactNode for custom label content.

onPrimaryAction

Optional • Type: () => void

A callback that is invoked when the user taps the menu label. When provided, a single tap triggers this action, while a long-press shows the menu. When not provided, a single tap shows the menu.

systemImage

Optional • Type: string

An SF Symbol name to display alongside the label. Only used when label is a string.

Inherited props

Modifiers

Use these modifiers in the modifiers prop of the SwiftUI version. The Drop-in version does not accept modifiers.

Expo UI lets you put any of the 89 modifiers for any view on Menu, but not all of them do something here.

  • 66 change what you see on Menu.
  • 10 have no visible effect on Menu.
  • 13 work only in one place, such as a widget or a Grid.

Where the modifiers go

modifiers apply to the button that opens the menu, not to the menu items.

Best modifiers for Menu

These modifiers are made for Menu, or they are the ones that you use most with it.

Also works on Menu

Menu takes these modifiers too, and each one works in the usual way.

No visible effect on Menu

Menu accepts these modifiers, but you see no change. They do not give an error.

ModifierWhy nothing happens
accessibilityElementIt joins the child views into one accessibility element, and Menu takes no child views.
containerShapeIt sets the shape that the child views use for containerRelativeShape, and Menu takes no child views.
geometryGroup iOS 17+It works on the child views that you give the component, and Menu takes none.
layoutPriorityIt has an effect only when the view shares an axis with other views in a stack.
onAppearIn SDK 57, Menu does not send modifier events to JavaScript, so the handler does not run.
onDisappearIn SDK 57, Menu does not send modifier events to JavaScript, so the handler does not run.
onGeometryChangeIn SDK 57, Menu does not send modifier events to JavaScript, so the handler does not run.
onLongPressGestureIn SDK 57, Menu does not send modifier events to JavaScript, so the handler does not run.
onTapGestureIn SDK 57, Menu does not send modifier events to JavaScript, so the handler does not run.
zIndexIt changes the order of views that cover each other in the same parent, and Menu does not stack its children.

Only in some places

These modifiers work only in a widget, in a Grid, in a Namespace or in a parent that asks for them.

ModifierWhere it works
activityBackgroundTintWorks only in a Live Activity. It has no effect in the app.
alignmentGuideHas an effect only when the parent, such as a VStack or ZStack, aligns its children by the same guide.
containerBackgroundWorks only in a widget, or in a navigation container when you use a navigation placement.
glassEffectIdWorks only on a view that also has glassEffect, in a GlassEffectContainer and a Namespace.
gridCellAnchor iOS 16+Works only on a cell of a Grid. Grid is in @expo/ui/swift-ui, but it has no Expo docs page.
gridCellColumnsWorks only on a view in a Grid.Row. Grid is in @expo/ui/swift-ui, but it has no Expo docs page.
gridCellUnsizedAxesWorks only on a view in a Grid. Grid is in @expo/ui/swift-ui, but it has no Expo docs page.
gridColumnAlignment iOS 16+Works only on a view in a Grid.Row. Grid is in @expo/ui/swift-ui, but it has no Expo docs page.
invalidatableContent iOS 17+Has an effect only when a parent view uses redacted with the invalidated reason.
matchedGeometryEffectWorks only in a Namespace. Give two views the same id and change between them with an animation.
privacySensitiveHas an effect only when a parent view uses redacted with the privacy reason.
unredactedHas an effect only in a parent view that uses redacted.
widgetURLWorks only in a widget. It has no effect in the app.

Sources

The original Expo docs for this page:

  • Menu (SwiftUI) (source-of-truth/versions/v57.0.0/sdk/ui/swift-ui/menu.md)
  • Menu (Drop-in) (source-of-truth/versions/v57.0.0/sdk/ui/drop-in-replacements/menu.md)

On this page