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>
);
}
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.
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.
accessibilityAddTraitsaccessibilityHiddenaccessibilityHintaccessibilityIdentifieraccessibilityInputLabelsaccessibilityLabelaccessibilityRemoveTraitsaccessibilityValueallowsTighteninganimationaspectRatiobackgroundbackgroundOverlayblurboldborderbrightnessclippedclipShapecolorInvertcontainerRelativeFrameiOS 17+contentShapecontentTransitioniOS 16+contrastcornerRadiusdisableddynamicTypeSizeenvironmentfixedSizeforegroundColorforegroundStyleglassEffectgrayscalehiddenhueRotationidignoreSafeAreaimageScaleitalickerninglineHeightiOS 26+lineLimitlineSpacingluminanceToAlphamaskminimumScaleFactormonospacedDigitmultilineTextAlignmentoffsetopacityoverlayredactedrotation3DEffectrotationEffectsaturationscaleEffectshadowstrikethroughstrokeBordertextCasetruncationModeunderline
No visible effect on Menu
Menu accepts these modifiers, but you see no change. They do not give an error.
| Modifier | Why nothing happens |
|---|---|
accessibilityElement | It joins the child views into one accessibility element, and Menu takes no child views. |
containerShape | It 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. |
layoutPriority | It has an effect only when the view shares an axis with other views in a stack. |
onAppear | In SDK 57, Menu does not send modifier events to JavaScript, so the handler does not run. |
onDisappear | In SDK 57, Menu does not send modifier events to JavaScript, so the handler does not run. |
onGeometryChange | In SDK 57, Menu does not send modifier events to JavaScript, so the handler does not run. |
onLongPressGesture | In SDK 57, Menu does not send modifier events to JavaScript, so the handler does not run. |
onTapGesture | In SDK 57, Menu does not send modifier events to JavaScript, so the handler does not run. |
zIndex | It 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.
| Modifier | Where it works |
|---|---|
activityBackgroundTint | Works only in a Live Activity. It has no effect in the app. |
alignmentGuide | Has an effect only when the parent, such as a VStack or ZStack, aligns its children by the same guide. |
containerBackground | Works only in a widget, or in a navigation container when you use a navigation placement. |
glassEffectId | Works 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. |
gridCellColumns | Works only on a view in a Grid.Row. Grid is in @expo/ui/swift-ui, but it has no Expo docs page. |
gridCellUnsizedAxes | Works 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. |
matchedGeometryEffect | Works only in a Namespace. Give two views the same id and change between them with an animation. |
privacySensitive | Has an effect only when a parent view uses redacted with the privacy reason. |
unredacted | Has an effect only in a parent view that uses redacted. |
widgetURL | Works 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)












