Context Menu

A context menu provides access to functionality that's directly related to an item, without cluttering the interface.

Pro component

Requires all-access to use the source code

Apple
Apple
1

Add the following dependencies to your project:

npx expo install react-native-keyboard-controller
import * as Haptics from 'expo-haptics';
import * as React from 'react';
import { Image, Platform, Pressable, ScrollView, View } from 'react-native';
import Animated, { ZoomIn, ZoomOut } from 'react-native-reanimated';

import { Avatar, AvatarFallback, AvatarImage } from '@/components/nativewindui/Avatar';
import { Button } from '@/components/nativewindui/Button';
import {
  addOpacityToRgb,
  Card,
  CardBadge,
  CardContent,
  CardImage,
  CardSubtitle,
  CardTitle,
} from '@/components/nativewindui/Card';
import { ContextMenu } from '@/components/nativewindui/ContextMenu';
import { createContextItem, createContextSubMenu } from '@/components/nativewindui/ContextMenu/utils';
import { Icon } from '@/components/nativewindui/Icon';
import { IconProps } from '@/components/nativewindui/Icon/types';
import { Text } from '@/components/nativewindui/Text';
import { cn } from '@/lib/cn';
import { useColorScheme } from '@/lib/useColorScheme';

export default function Screen() {
  return (
    <ScrollView contentContainerClassName="gap-6 p-4 py-6">
      <ProjectCard />
      <MessageCard />
      <OrderCard />
      <View className="pb-safe" />
    </ScrollView>
  );
}

const IOS_PREVIEW_CONFIG = {
  borderRadius: 16,
} as const;

const PROJECT_IMAGE_SOURCE = {
  uri: 'https://images.unsplash.com/photo-1517048676732-d65bc937f952?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
};

function ProjectCard() {
  const { colors } = useColorScheme();
  const [isFavorite, setIsFavorite] = React.useState(false);

  const projectContextMenuItems = React.useMemo(
    () => [
      createContextItem({ actionKey: 'open', title: 'Open' }),
      createContextItem({
        actionKey: 'mark-as-favorite',
        title: isFavorite ? 'Unmark as Favorite' : 'Mark as Favorite',
        icon: Platform.select({
          ios: { name: isFavorite ? 'heart.fill' : 'heart' },
        }),
        state: Platform.select({ android: { checked: isFavorite } }),
        keepOpenOnPress: true,
      }),
      createContextItem({ actionKey: 'rename', title: 'Rename' }),
      createContextSubMenu(
        {
          title: 'Share this project',
          iOSItemSize: 'small',
          iOSType: 'inline',
        },
        [
          createContextItem({
            actionKey: 'link',
            title: 'Link',
            icon: { name: 'link' },
          }),
          createContextItem({
            actionKey: 'email',
            title: 'Email',
            icon: { name: 'envelope' },
          }),
          createContextItem({
            actionKey: 'message',
            title: 'Message',
            icon: { name: 'message' },
          }),
        ]
      ),
    ],
    [isFavorite]
  );

  return (
    <ContextMenu
      items={projectContextMenuItems}
      auxiliaryPreviewPosition="center"
      iosPreviewConfig={IOS_PREVIEW_CONFIG}
      materialMinWidth={240}
      iosRenderPreview={() => {
        return <Image className="aspect-square w-screen" source={PROJECT_IMAGE_SOURCE} />;
      }}
      onItemPress={(item) => {
        if (item.actionKey === 'mark-as-favorite') {
          setIsFavorite((prev) => !prev);
        }
      }}>
      <Pressable className="ios:rounded-2xl android:border-muted/70 border-muted rounded-xl border border-dashed">
        <Card>
          <CardImage
            source={PROJECT_IMAGE_SOURCE}
            materialRootClassName="absolute top-0 left-0 right-0 bottom-0 rounded-xl"
          />
          <CardBadge>
            <Text>Long press me</Text>
          </CardBadge>
          <CardContent
            className="pt-48"
            linearGradientColors={[
              'transparent',
              'transparent',
              addOpacityToRgb(colors.card, 0.8),
              colors.card,
            ]}>
            <CardTitle>Project Proposal</CardTitle>
            <CardSubtitle>Updated yesterday</CardSubtitle>
          </CardContent>
        </Card>
      </Pressable>
    </ContextMenu>
  );
}

const EMOJI_TO_ICON: Record<string, IconProps> = {
  love: {
    name: 'heart.fill',
  },
  like: {
    sfSymbol: { name: 'hand.thumbsup.fill' },
    materialCommunityIcon: {
      name: 'thumb-up',
    },
  },
  dislike: {
    sfSymbol: { name: 'hand.thumbsdown.fill' },
    materialCommunityIcon: {
      name: 'thumb-down',
    },
  },
  exclamation: {
    name: 'exclamationmark',
  },
  question: {
    name: 'questionmark',
  },
};

const AVATAR_SOURCE = { uri: 'https://avatars.githubusercontent.com/u/63797719?v=4' };

type SelectedEmoji = keyof typeof EMOJI_TO_ICON | null;

function MessageCard() {
  const [selectedEmoji, setSelectedEmoji] = React.useState<SelectedEmoji>(null);
  const [isPinned, setIsPinned] = React.useState(false);

  const messageContextMenuItems = [
    createContextItem({
      actionKey: 'pin',
      title: isPinned ? 'Unpin' : 'Pin',
      icon: { name: isPinned ? 'pin.fill' : 'pin' },
      keepOpenOnPress: true,
    }),
    createContextItem({
      actionKey: 'forward',
      title: 'Forward',
      icon: { name: 'arrowshape.turn.up.right' },
    }),
    createContextItem({
      actionKey: 'delete',
      title: 'Delete',
      destructive: true,
      icon: { name: 'trash' },
    }),
  ];

  function handleEmojiPress(emoji: string) {
    return () => {
      if (emoji === selectedEmoji) {
        setSelectedEmoji('');
        return;
      }
      Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
      setSelectedEmoji(emoji);
    };
  }

  return (
    <ContextMenu
      items={messageContextMenuItems}
      materialAlign="start"
      auxiliaryPreviewPosition="center"
      iosPreviewConfig={IOS_PREVIEW_CONFIG}
      onItemPress={(item) => {
        if (item.actionKey === 'pin') {
          setIsPinned((prev) => !prev);
        }
      }}
      renderAuxiliaryPreview={() => {
        return (
          <View
            className={cn(
              'bg-card flex-row gap-1 rounded-full p-0.5',
              Platform.OS === 'ios' && 'bg-card/60 dark:bg-border/70'
            )}>
            <EmojiButton
              onPress={handleEmojiPress('love')}
              selectedEmoji={selectedEmoji}
              name="love"
            />
            <EmojiButton
              onPress={handleEmojiPress('like')}
              selectedEmoji={selectedEmoji}
              name="like"
            />
            <EmojiButton
              onPress={handleEmojiPress('dislike')}
              selectedEmoji={selectedEmoji}
              name="dislike"
            />
            <EmojiButton
              onPress={handleEmojiPress('exclamation')}
              selectedEmoji={selectedEmoji}
              name="exclamation"
            />
            <EmojiButton
              onPress={handleEmojiPress('question')}
              selectedEmoji={selectedEmoji}
              name="question"
            />
          </View>
        );
      }}>
      <Pressable className="ios:rounded-2xl android:border-muted/70 border-muted rounded-xl border border-dashed">
        <Card>
          <CardBadge className="bg-muted/20 dark:bg-muted/30 android:opacity-70 -z-10">
            <Text>Long press me</Text>
          </CardBadge>
          <CardContent className="ios:pt-10 pt-4">
            <View className="flex-row items-center gap-3">
              <Avatar alt="Zach Nugent's Avatar" className="h-16 w-16">
                <AvatarImage source={AVATAR_SOURCE} />
                <AvatarFallback>
                  <Text className="dark:android:text-background text-white">ZN</Text>
                </AvatarFallback>
              </Avatar>
              <View>
                <Text variant="caption2" className="text-muted-foreground px-1 pb-1">
                  Zach Nugent
                </Text>
                <View
                  className={cn(
                    'bg-background dark:bg-muted-foreground relative rounded-2xl px-3 py-1.5',
                    Platform.OS === 'ios' && 'dark:bg-muted'
                  )}>
                  <Text>Hey, got a minute?</Text>
                  {!selectedEmoji ? null : (
                    <Animated.View
                      key={selectedEmoji}
                      entering={ZoomIn.springify().damping(22).mass(0.9).stiffness(350)}
                      exiting={ZoomOut.duration(150)}
                      className={cn('bg-card absolute -top-4 rounded-full p-px', '-right-3')}>
                      <View className="bg-primary rounded-full p-1">
                        <Icon {...EMOJI_TO_ICON[selectedEmoji]} size={18} color="white" />
                        {Platform.OS === 'ios' && (
                          <>
                            <View
                              className={cn(
                                'bg-primary absolute bottom-0 h-2 w-2 rounded-full',
                                'right-0'
                              )}
                            />
                            <View
                              className={cn(
                                'bg-primary absolute -bottom-1 h-1 w-1 rounded-full',
                                '-right-1'
                              )}
                            />
                          </>
                        )}
                      </View>
                    </Animated.View>
                  )}
                </View>
              </View>
            </View>
          </CardContent>
        </Card>
      </Pressable>
    </ContextMenu>
  );
}

function EmojiButton({
  onPress,
  selectedEmoji,
  name,
}: {
  onPress: () => void;
  selectedEmoji: SelectedEmoji;
  name: string;
}) {
  const { colors } = useColorScheme();

  return (
    <Button size="icon" variant="plain" onPress={onPress} className="ios:rounded-full rounded-full">
      {selectedEmoji === name && (
        <Animated.View
          entering={ZoomIn.springify().damping(22).mass(0.9).stiffness(350)}
          exiting={ZoomOut.duration(150)}
          className="bg-primary absolute bottom-0 left-0 right-0 top-0 rounded-full"
        />
      )}
      <Icon {...EMOJI_TO_ICON[name]} color={selectedEmoji === name ? 'white' : colors.grey} />
    </Button>
  );
}

const ORDER_IMAGE_SOURCE = {
  uri: 'https://images.unsplash.com/photo-1644079446600-219068676743?q=80&w=1740&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
};

function OrderCard() {
  const { colors } = useColorScheme();
  const [isLoading, setIsLoading] = React.useState(true);
  const [status, setStatus] = React.useState('pending');

  const orderContextMenuItems = React.useMemo(
    () => [
      createContextItem({ actionKey: 'view-details', title: 'View Details' }),
      createContextItem({ actionKey: 'edit-order', title: 'Edit Order' }),
      createContextItem({ actionKey: 'cancel-order', title: 'Cancel Order', destructive: true }),
      createContextSubMenu({ title: 'Change Status' }, [
        createContextItem({
          actionKey: 'pending',
          title: 'Pending',
          state: { checked: status === 'pending' },
          keepOpenOnPress: Platform.OS === 'android',
        }),
        createContextItem({
          actionKey: 'shipped',
          title: 'Shipped',
          state: { checked: status === 'shipped' },
        }),
        createContextItem({
          actionKey: 'delivered',
          title: 'Delivered',
          state: { checked: status === 'delivered' },
        }),
      ]),
      createContextItem({
        actionKey: 'download-invoice',
        title: 'Download Invoice',
        icon: { name: 'doc' },
        loading: isLoading,
        keepOpenOnPress: true,
      }),
    ],
    [isLoading, status]
  );

  return (
    <ContextMenu
      title="Order #8234"
      items={orderContextMenuItems}
      materialMinWidth={230}
      materialAlign="start"
      auxiliaryPreviewPosition="center"
      iosPreviewConfig={IOS_PREVIEW_CONFIG}
      onItemPress={(item) => {
        if (item.actionKey === 'download-invoice') {
          if (!isLoading) {
            setIsLoading(true);
            setTimeout(() => {
              setIsLoading(false);
            }, 1500);
          }
        }
        if (item.actionKey === 'pending') {
          setStatus('pending');
        }
        if (item.actionKey === 'shipped') {
          setStatus('shipped');
        }
        if (item.actionKey === 'delivered') {
          setStatus('delivered');
        }
      }}>
      <Pressable
        className="ios:rounded-2xl android:border-muted/70 border-muted rounded-xl border border-dashed"
        onLongPress={() => {
          if (isLoading) {
            setTimeout(() => {
              setIsLoading(false);
            }, 1500);
          }
        }}>
        <Card>
          <CardImage
            source={ORDER_IMAGE_SOURCE}
            materialRootClassName="absolute top-0 left-0 right-0 bottom-0 rounded-xl"
          />
          <CardBadge>
            <Text>Long press me</Text>
          </CardBadge>
          <CardContent
            className="pt-48"
            linearGradientColors={[
              'transparent',
              'transparent',
              addOpacityToRgb(colors.card, 0.8),
              colors.card,
            ]}>
            <CardTitle>Order #8234</CardTitle>
            <CardSubtitle>Placed 2 days ago</CardSubtitle>
          </CardContent>
        </Card>
      </Pressable>
    </ContextMenu>
  );
}
Logo

Unlock All Pro Templates & Components

Elevate your app with powerful, native-feeling components and templates. Build faster with beautiful ready-to-use designs.

Get all-access

Usage

The child component needs to accept Pressable props to ensure compatibility on Android.

import { ContextMenu } from '@/components/nativewindui/ContextMenu';
import {
  createContextItem,
  createContextSubMenu,
} from '@/components/nativewindui/ContextMenu/utils';
<ContextMenu
  className="rounded-md"
  items={[
    createContextItem({
      actionKey: 'first',
      title: 'Item 1',
    }),
    createContextSubMenu({ title: 'Submenu 1', iOSItemSize: 'small' }, [
      createContextItem({
        actionKey: 'sub-first',
        title: 'Sub Item 1',
      }),
      createContextItem({
        actionKey: 'sub-second',
        title: 'Sub Item 2',
      }),
    ]),
  ]}
  onItemPress={(item) => {
    console.log('Item Pressed', item);
  }}>
  <Pressable>
    <Text>Long Press Me</Text>
  </Pressable>
</ContextMenu>

Props

ContextMenu

Inherits all the props from React Native's View component.

PropTypeDefaultDescription
titlestringThe title of the context menu.
items(ContextItem | ContextSubMenu)[]The items or submenus to display in the context menu.
iOSItemSize'small' | 'medium' | 'large'The preferred item size on iOS.
childrenReact.ReactNodeThe child component to render inside the context menu. It needs to accept Pressable props to ensure compatibility on Android
onItemPress(item: Omit<ContextItem, 'icon'>) => voidCallback function triggered when an item is pressed.
enabledbooleantrueWhether the context menu is enabled.
iosRenderPreview() => React.ReactElementFunction to render a preview on iOS.
iosOnPressMenuPreview() => voidCallback for pressing the menu preview on iOS.
renderAuxiliaryPreview() => React.ReactElementFunction to render an auxiliary preview.
auxiliaryPreviewPosition'start' | 'center' | 'end'Position for the auxiliary preview.
materialPortalHoststringThe host for the Android portal.
materialSideOffsetnumber2Side offset for Android context menu.
materialAlignOffsetnumberAlignment offset for Android context menu.
materialAlign'start' | 'center' | 'end'Alignment for Android context menu.
materialWidthnumberWidth for the Android context menu.
materialMinWidthnumberMinimum width for the Android context menu.
materialLoadingTextstringText to display while the context menu is loading.
materialSubMenuTitlePlaceholderstringPlaceholder title for the Android submenu.
materialOverlayClassNamestringClass name for the Android overlay.

Utils

createContextSubMenu: A utility function to create a context sub-menu.


createContextItem: A utility function to create a context item.


import { createContextSubMenu, createContextItem } from '@/components/nativewindui/ContextMenu/utils';

const MENU = [
createContextSubMenu({ title: 'Submenu' }, [
  createContextItem({ actionKey: '1', title: 'Item 1' }),
  createContextItem({ actionKey: '2', title: 'Item 2' }),
]),
];
© Ronin Technologies LLC 2025