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 { Stack } from 'expo-router';
import * as React from 'react';
import { Platform, Pressable, View } from 'react-native';
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated';

import { Text } from '~/components/nativewindui/Text';
import { AlertAnchor } from '~/components/nativewindui/Alert';
import { AlertRef } from '~/components/nativewindui/Alert/types';
import { Button } from '../~/components/nativewindui/Button';
import { ContextMenu } from '~/components/nativewindui/ContextMenu';
import { ContextMenuRef } from '~/components/nativewindui/ContextMenu/types';
import {
  createContextItem,
  createContextSubMenu,
} from '~/components/nativewindui/ContextMenu/utils';
import { useColorScheme } from '~/lib/useColorScheme';

export default function ContextMenuScreen() {
  const [checked, setChecked] = React.useState(false);
  const [isLoading, setIsLoading] = React.useState(true);
  const { colors } = useColorScheme();
  const ref = React.useRef<ContextMenuRef>(null);
  const [selectedEmoji, setSelectedEmoji] = React.useState('');
  const alertRef = React.useRef<AlertRef>(null);

  const dynamicItems = React.useMemo(() => {
    return [
      createContextSubMenu({ title: 'Sub Menu', iOSItemSize: 'small', loading: isLoading }, [
        createContextSubMenu({ title: 'Submenu 2' }, [
          { actionKey: '1', title: 'Item 1' },
          { actionKey: '2', title: 'Item 2' },
        ]),
        createContextItem({ actionKey: '43', title: 'Item 3' }),
      ]),
      createContextItem({
        actionKey: '4',
        title: 'Checkbox Item',
        state: { checked },
        keepOpenOnPress: true,
        icon: {
          namingScheme: 'sfSymbol',
          name: 'checkmark.seal',
          color: colors.primary,
        },
      }),
      createContextItem({
        actionKey: '5',
        title: 'Set to loading',
        keepOpenOnPress: true,
        disabled: isLoading,
      }),
    ];
  }, [checked, isLoading]);

  function handleEmojiPress(emoji: string) {
    return () => {
      if (emoji === selectedEmoji) {
        return;
      }
      setSelectedEmoji(emoji);
      ref.current?.dismissMenu?.();
    };
  }

  return (
    <>
      <Stack.Screen options={{ title: 'Context Menu' }} />
      <View className="flex-1  justify-center gap-8 p-8">
        <ContextMenu
          className="rounded-md"
          title="Dropdown Menu"
          items={STATIC_ITEMS}
          materialAlign="start"
          auxiliaryPreviewPosition="center"
          renderAuxiliaryPreview={() => {
            return (
              <Animated.View
                entering={FadeIn}
                exiting={FadeOut}
                className="flex-row items-center justify-center rounded-md bg-red-500 px-12 py-4">
                <Text variant="footnote">Auxiliary Preview</Text>
              </Animated.View>
            );
          }}
          onItemPress={(item) => {
            alertRef.current?.alert({
              title: 'Item Pressed',
              message: `Item ${item.actionKey} pressed`,
              buttons: [{ text: 'OK' }],
              materialWidth: 350,
            });
          }}>
          <Pressable className="border-foreground bg-card h-32 items-center justify-center rounded-md border border-dashed">
            <Text variant="footnote" className="text-muted-foreground font-bold">
              Static
            </Text>
            <Text>Long Press Me</Text>
          </Pressable>
        </ContextMenu>

        <ContextMenu
          ref={ref}
          className="rounded-md"
          items={dynamicItems}
          auxiliaryPreviewPosition="center"
          renderAuxiliaryPreview={() => {
            return (
              <Animated.View
                entering={FadeIn}
                exiting={FadeOut}
                className="bg-card flex-row rounded-md p-2 shadow">
                <Button
                  variant={selectedEmoji === '❤️' ? 'tonal' : 'plain'}
                  size="icon"
                  onPress={handleEmojiPress('❤️')}>
                  <Text variant="footnote">❤️</Text>
                </Button>
                <Button
                  variant={selectedEmoji === '😍' ? 'tonal' : 'plain'}
                  size="icon"
                  onPress={handleEmojiPress('😍')}>
                  <Text variant="footnote">😍</Text>
                </Button>
                <Button
                  variant={selectedEmoji === '🥰' ? 'tonal' : 'plain'}
                  size="icon"
                  onPress={handleEmojiPress('🥰')}>
                  <Text variant="footnote">🥰</Text>
                </Button>
                <Button
                  variant={selectedEmoji === '💘' ? 'tonal' : 'plain'}
                  size="icon"
                  onPress={handleEmojiPress('💘')}>
                  <Text variant="footnote">💘</Text>
                </Button>
              </Animated.View>
            );
          }}
          onItemPress={(item) => {
            if (item.actionKey === '4') {
              setChecked((prev) => !prev);
              return;
            }
            if (item.actionKey === '5') {
              setIsLoading(true);
              setTimeout(() => {
                setIsLoading(false);
              }, 1500);
              return;
            }
            alertRef.current?.alert({
              title: 'Item Pressed',
              message: `Item ${item.actionKey} pressed`,
              buttons: [{ text: 'OK' }],
              materialWidth: 350,
            });
          }}>
          <Pressable
            onLongPress={() => {
              if (isLoading) {
                setTimeout(() => {
                  setIsLoading(false);
                }, 1500);
              }
            }}
            className="border-primary bg-card h-32 items-center justify-center rounded-md border border-dashed">
            <Text variant="footnote" className="text-muted-foreground font-bold">
              With State
            </Text>
            <Text variant="footnote">Checked: {checked ? 'true' : 'false'}</Text>
            <Text variant="footnote">Emoji: {selectedEmoji}</Text>
          </Pressable>
        </ContextMenu>
        {Platform.OS === 'ios' && (
          <ContextMenu
            className="rounded-md"
            title="Dropdown Menu"
            items={STATIC_ITEMS}
            materialAlign="start"
            auxiliaryPreviewPosition="center"
            renderAuxiliaryPreview={() => {
              return (
                <Animated.View
                  entering={FadeIn}
                  exiting={FadeOut}
                  className="bg-card flex-row items-center justify-center rounded-md px-12 py-4">
                  <Text variant="footnote">Auxiliary Preview</Text>
                </Animated.View>
              );
            }}
            iosRenderPreview={() => {
              return (
                <View className="aspect-square h-72 items-center justify-center rounded-md bg-red-500 px-12 py-4">
                  <Text variant="footnote">iOS Preview</Text>
                </View>
              );
            }}
            onItemPress={(item) => {
              alertRef.current?.alert({
                title: 'Item Pressed',
                message: `Item ${item.actionKey} pressed`,
                buttons: [{ text: 'OK' }],
                materialWidth: 350,
              });
            }}>
            <Pressable className="border-accent bg-card h-32 items-center justify-center rounded-md border border-dashed">
              <Text variant="footnote" className="text-muted-foreground font-bold">
                With Preview
              </Text>
              <Text>Long Press Me</Text>
            </Pressable>
          </ContextMenu>
        )}
      </View>
      <AlertAnchor ref={alertRef} />
    </>
  );
}

const STATIC_ITEMS = [
  createContextSubMenu({ title: 'Submenu 1', iOSItemSize: 'small', loading: false }, [
    createContextSubMenu({ title: 'Sub', iOSItemSize: 'small' }, [
      { actionKey: '10', title: 'Select Me' },
      { actionKey: '20', title: 'No! Select Me!' },
    ]),
    createContextItem({
      actionKey: '430',
      title: 'Item 430',
      icon: { name: 'checkmark.seal', namingScheme: 'sfSymbol' },
    }),
  ]),
  createContextSubMenu({ title: 'Hello', iOSItemSize: 'small' }, [
    createContextItem({
      actionKey: '30',
      icon: { name: 'checkmark.seal', namingScheme: 'sfSymbol' },
    }),
    createContextItem({
      actionKey: '31',
      icon: { name: 'checkmark.seal', namingScheme: 'sfSymbol' },
    }),
    createContextItem({
      actionKey: '32',
      icon: { name: 'checkmark.seal', namingScheme: 'sfSymbol' },
    }),
    createContextItem({
      actionKey: '33',
      icon: { name: 'checkmark.seal', namingScheme: 'sfSymbol' },
    }),
  ]),
  createContextSubMenu({ title: '', iOSType: 'inline', iOSItemSize: 'small' }, [
    createContextItem({
      actionKey: '130',
      title: '💧',
    }),
    createContextItem({
      actionKey: '131',
      title: '💧',
    }),
    createContextItem({
      actionKey: '132',
      title: '💧',
    }),
    createContextItem({
      actionKey: '133',
      title: '💧',
    }),
  ]),
  createContextItem({
    actionKey: '40',
    title: 'Delete Computer',
    destructive: true,
    image: { url: 'https://picsum.photos/id/2/100', cornerRadius: 30 },
  }),
];
Logo

Unlock All Pro Screens & Components

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

Get all-access

Usage

The Context-Menu is not compatible with the new architecture yet .

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'>, isUsingActionSheetFallback?: boolean) => 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 2024