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 usebuttonStyleinstead, 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
Menu
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
accessibilityAddTraitsaccessibilityElementaccessibilityHiddenaccessibilityHintaccessibilityIdentifieraccessibilityInputLabelsaccessibilityLabelaccessibilityRemoveTraitsaccessibilityValueactivityBackgroundTintalignmentGuideallowsTighteninganimationaspectRatiobackgroundbackgroundOverlayblurboldborderbrightnessclippedclipShapecolorInvertcontainerBackgroundcontainerRelativeFrameiOS 17+containerShapecontentShapecontentTransitioniOS 16+contrastcornerRadiuscreateModifierdisableddynamicTypeSizeenvironmentfixedSizefontforegroundColorforegroundStyleframegeometryGroupiOS 17+glassEffectglassEffectIdgrayscalegridCellAnchoriOS 16+gridCellColumnsgridCellUnsizedAxesgridColumnAlignmentiOS 16+hiddenhueRotationidignoreSafeAreaimageScaleinvalidatableContentiOS 17+italickerninglayoutPrioritylineHeightiOS 26+lineLimitlineSpacingluminanceToAlphamaskmatchedGeometryEffectminimumScaleFactormonospacedDigitmultilineTextAlignmentoffsetonAppearonDisappearonGeometryChangeonLongPressGestureonTapGestureopacityoverlaypaddingprivacySensitiveredactedrotation3DEffectrotationEffectsaturationscaleEffectshadowstrikethroughstrokeBordertextCasetinttruncationModeunderlineunredactedwidgetURLzIndex
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)