Expo UI and Router for iOS
Components

Menu

A SwiftUI Menu component for displaying dropdown menus.

Expo UI Menu matches the official SwiftUI Menu API and supports styling via the buttonStyle modifier. Menu opens on a single tap. For long-press interactions, use ContextMenu 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>
  );
}

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>
  );
}

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>
  );
}

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>
  );
}

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>
  );
}

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>
  );
}

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>
  );
}

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>
  );
}

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>
  );
}

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>
  );
}

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>
  );
}

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>
  );
}

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>
  );
}

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.

Modifiers for Menu

Modifiers for any view

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