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

ContextMenu

A SwiftUI ContextMenu component for displaying context menus.

Expo UI ContextMenu matches the official SwiftUI contextMenu API and displays a menu when long-pressed. For single-tap menu interactions, use Menu instead.

Usage

Basic context menu

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

export default function BasicContextMenuExample() {
  return (
    <Host style={{ flex: 1 }}>
      <ContextMenu>
        <ContextMenu.Items>
          <Button
            label="Edit"
            onPress={() => console.log('Edit')}
          />
          <Button
            label="Delete"
            role="destructive"
            onPress={() => console.log('Delete')}
          />
        </ContextMenu.Items>
        <ContextMenu.Trigger>
          <Text>Long press me</Text>
        </ContextMenu.Trigger>
      </ContextMenu>
    </Host>
  );
}
A context menu open under the lifted trigger, listing Edit and a red Delete
A context menu open under the lifted trigger, listing Edit and a red Delete

Context menu with system images

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

export default function ContextMenuWithImagesExample() {
  return (
    <Host style={{ flex: 1 }}>
      <ContextMenu>
        <ContextMenu.Items>
          <Button
            label="Share"
            systemImage="square.and.arrow.up"
            onPress={() => console.log('Share')}
          />
          <Button
            label="Favorite"
            systemImage="heart"
            onPress={() => console.log('Favorite')}
          />
          <Button
            label="Delete"
            systemImage="trash"
            role="destructive"
            onPress={() => console.log('Delete')}
          />
        </ContextMenu.Items>
        <ContextMenu.Trigger>
          <Text>Long press me</Text>
        </ContextMenu.Trigger>
      </ContextMenu>
    </Host>
  );
}
A context menu listing Share, Favorite, and a red Delete, each with an SF Symbol
A context menu listing Share, Favorite, and a red Delete, each with an SF Symbol

Context menu with preview

Use ContextMenu.Preview to show a custom preview above the menu when opened.

import { View, Text as RNText } from 'react-native';
import {
  Host,
  ContextMenu,
  Button,
  RNHostView,
  Text,
} from '@expo/ui/swift-ui';

export default function ContextMenuWithPreviewExample() {
  return (
    <Host style={{ flex: 1 }}>
      <ContextMenu>
        <ContextMenu.Items>
          <Button
            label="Edit"
            onPress={() => console.log('Edit')}
          />
          <Button
            label="Delete"
            role="destructive"
            onPress={() => console.log('Delete')}
          />
        </ContextMenu.Items>
        <ContextMenu.Trigger>
          <Text>Long press me</Text>
        </ContextMenu.Trigger>
        <ContextMenu.Preview>
          <RNHostView matchContents>
            <View
              style={{
                width: 200,
                height: 100,
                backgroundColor: '#f0f0f0',
                padding: 16,
              }}>
              <RNText>Preview content</RNText>
            </View>
          </RNHostView>
        </ContextMenu.Preview>
      </ContextMenu>
    </Host>
  );
}
A context menu with a Preview content card above the Edit and Delete items
A context menu with a Preview content card above the Edit and Delete items

Context menu with picker

import { useState } from 'react';
import {
  Host,
  ContextMenu,
  Button,
  Text,
  Picker,
} from '@expo/ui/swift-ui';
import { pickerStyle, tag } from '@expo/ui/swift-ui/modifiers';

export default function ContextMenuWithPickerExample() {
  const [selectedIndex, setSelectedIndex] = useState<
    number | undefined
  >(0);

  return (
    <Host style={{ flex: 1 }}>
      <ContextMenu>
        <ContextMenu.Items>
          <Button
            label="Action"
            onPress={() => console.log('Action')}
          />
          <Picker
            label="Size"
            modifiers={[pickerStyle('menu')]}
            selection={selectedIndex}
            onSelectionChange={setSelectedIndex}>
            {['Small', 'Medium', 'Large'].map((option, index) => (
              <Text key={index} modifiers={[tag(index)]}>
                {option}
              </Text>
            ))}
          </Picker>
        </ContextMenu.Items>
        <ContextMenu.Trigger>
          <Text>Long press me</Text>
        </ContextMenu.Trigger>
      </ContextMenu>
    </Host>
  );
}
A context menu listing Action and a Size row with a submenu chevron
A context menu listing Action and a Size row with a submenu chevron

Context menu with sections

Use Section and Divider components to organize menu items.

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

export default function ContextMenuWithSectionsExample() {
  return (
    <Host style={{ flex: 1 }}>
      <ContextMenu>
        <ContextMenu.Items>
          <Section title="Actions">
            <Button
              label="Edit"
              onPress={() => console.log('Edit')}
            />
            <Button
              label="Duplicate"
              onPress={() => console.log('Duplicate')}
            />
          </Section>
          <Divider />
          <Button
            label="Delete"
            role="destructive"
            onPress={() => console.log('Delete')}
          />
        </ContextMenu.Items>
        <ContextMenu.Trigger>
          <Text>Long press me</Text>
        </ContextMenu.Trigger>
      </ContextMenu>
    </Host>
  );
}
A context menu with an Actions section holding Edit and Duplicate, a divider, and Delete
A context menu with an Actions section holding Edit and Duplicate, a divider, and Delete

Disabled items

Use the disabled(true) modifier on a menu Button to render it greyed-out and non-interactive.

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

export default function DisabledContextMenuItemExample() {
  return (
    <Host style={{ flex: 1 }}>
      <ContextMenu>
        <ContextMenu.Items>
          <Button
            label="Edit"
            onPress={() => console.log('Edit')}
          />
          <Button
            label="Locked"
            systemImage="lock"
            modifiers={[disabled(true)]}
            onPress={() => console.log('This never fires')}
          />
        </ContextMenu.Items>
        <ContextMenu.Trigger>
          <Text>Long press me</Text>
        </ContextMenu.Trigger>
      </ContextMenu>
    </Host>
  );
}
A context menu listing Edit and a greyed out Locked item with a lock icon
A context menu listing Edit and a greyed out Locked item with a lock icon

Selectable items (checkmarks)

A SwiftUI Toggle inside ContextMenu.Items automatically renders as a row with a leading SF Symbol and a trailing checkmark when isOn is true.

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

export default function CheckmarkContextMenuItemExample() {
  const [pinned, setPinned] = useState(false);

  return (
    <Host style={{ flex: 1 }}>
      <ContextMenu>
        <ContextMenu.Items>
          <Toggle
            isOn={pinned}
            label="Pin"
            systemImage="pin"
            onIsOnChange={setPinned}
          />
        </ContextMenu.Items>
        <ContextMenu.Trigger>
          <Text>Long press me</Text>
        </ContextMenu.Trigger>
      </ContextMenu>
    </Host>
  );
}
A context menu row labelled Pin with a pin icon and a leading checkmark
A context menu row labelled Pin with a pin icon and a leading checkmark

Nested context menus

Use nested ContextMenu components to create submenus.

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

export default function NestedContextMenuExample() {
  return (
    <Host style={{ flex: 1 }}>
      <ContextMenu>
        <ContextMenu.Items>
          <Button
            label="Action"
            onPress={() => console.log('Action')}
          />
          <ContextMenu>
            <ContextMenu.Items>
              <Button
                label="Sub Action 1"
                onPress={() => console.log('Sub 1')}
              />
              <Button
                label="Sub Action 2"
                onPress={() => console.log('Sub 2')}
              />
            </ContextMenu.Items>
            <ContextMenu.Trigger>
              <Button label="More Options" />
            </ContextMenu.Trigger>
          </ContextMenu>
        </ContextMenu.Items>
        <ContextMenu.Trigger>
          <Text>Long press me</Text>
        </ContextMenu.Trigger>
      </ContextMenu>
    </Host>
  );
}
A context menu listing Action and More Options
A context menu listing Action and More Options

API

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

Components

ContextMenu

Type: React.Element<ContextMenuProps>

ContextMenu allows you to create a context menu, which can be used to provide additional options to the user.

Props of the ContextMenu component.

ContextMenuProps

children

Type: ReactNode

The contents of the context menu. Should include ContextMenu.Trigger, ContextMenu.Items, and optionally ContextMenu.Preview.

Inherited props

Items

Type: React.Element<{ children: ReactNode }>

Items visible inside the context menu. It could be Section, Divider, Button, Toggle, Picker or even ContextMenu itself for nested menus. Remember to use components from the @expo/ui/swift-ui library.

Preview

Type: React.Element<{ children: ReactNode }>

The component visible above the menu when it is opened.

Trigger

Type: React.Element<{ children: ReactNode }>

The component visible all the time that triggers the context menu when long-pressed.

Modifiers

Use these modifiers in the modifiers prop of the SwiftUI version.

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

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

Where the modifiers go

modifiers apply to the ContextMenu.Trigger view, not to the menu items or the preview.

Best modifiers for ContextMenu

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

Also works on ContextMenu

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

No visible effect on ContextMenu

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

ModifierWhy nothing happens
layoutPriorityIt has an effect only when the view shares an axis with other views in a stack.
onAppearIn SDK 57, ContextMenu does not send modifier events to JavaScript, so the handler does not run.
onDisappearIn SDK 57, ContextMenu does not send modifier events to JavaScript, so the handler does not run.
onGeometryChangeIn SDK 57, ContextMenu does not send modifier events to JavaScript, so the handler does not run.
onLongPressGestureIn SDK 57, ContextMenu does not send modifier events to JavaScript, so the handler does not run.
onTapGestureIn SDK 57, ContextMenu 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 ContextMenu 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:

On this page