Context menu
Free · MITReact Native · ExpoLong-press an element and it lifts out of the page above a dimmed backdrop while a menu springs out beside it; keep the finger down, slide onto an item and release to choose. Right-click opens it on the web.
Installation
$ npx shadcn@latest add https://goodui.dev/native/r/context-menu.jsonThen run npx expo install --fix so react-native-gesture-handler, react-native-reanimated match your Expo SDK. Files land in components/goodui-native/context-menu.
- Wrap the app in GestureHandlerRootView and ThemeProvider (the menu renders in its overlay layer).
Usage
import { ContextMenu } from "@/components/goodui-native/context-menu/context-menu";
<ContextMenu
accessibilityLabel={`${message.author}: ${message.text}`}
previewStyle={{ borderRadius: 18 }}
groups={[
[
{ key: "reply", label: "Reply", icon: (p) => <ReplyIcon {...p} />, onPress: () => reply(message) },
{ key: "copy", label: "Copy", icon: (p) => <CopyIcon {...p} />, onPress: () => copy(message.text) },
],
[{ key: "delete", label: "Delete", destructive: true, icon: (p) => <TrashIcon {...p} />, onPress: () => remove(message.id) }],
]}
>
<MessageBubble message={message} />
</ContextMenu>When to use
- Secondary actions on content people browse: messages, photos, files, cards.
- Actions that are also reachable elsewhere, so the menu is a shortcut and not the only path.
- The main action of an element: put it in onPress.
- Choices not tied to a visible element: use action-sheet.
- Rows in a list where swiping is the expected shortcut: use swipe-actions.
- Explaining something: use popover.
Examples
Tap to open, hold for more
<ContextMenu
title="Lisbon, October 2026"
onPress={() => openPhoto(photo.id)}
actions={[
{ key: "share", label: "Share", icon: (p) => <ShareIcon {...p} />, onPress: share },
{ key: "delete", label: "Delete", destructive: true, onPress: remove },
]}
>
<PhotoTile photo={photo} />
</ContextMenu>Menu only, no lift
<ContextMenu variant="menu" onPress={open} actions={[{ key: "rename", label: "Rename", onPress: rename }]}>
<FileRow file={file} />
</ContextMenu>API reference
| Prop | Type | Default |
|---|---|---|
| children* The element that lifts. Keep it non-interactive and put its tap in `onPress`. | ReactNode | — |
| actions One group of actions. Use `groups` for several. | ContextMenuAction[] | — |
| groups Several groups separated by a thick divider. | ContextMenuAction[][] | — |
| title Small heading at the top of the menu. | string | — |
| onPress A normal tap on the element. | () => void | — |
| onOpenChange Called when the menu opens or closes. | (open: boolean) => void | — |
| variant `lift` raises a copy of the element above a dimmed backdrop; `menu` only shows the menu next to it. | "lift" | "menu" | "lift" |
| delayLongPress Hold duration in milliseconds before the menu opens. Default 350. | number | 350 |
| menuWidth Menu width in points. Default 250. | number | 250 |
| previewStyle Shape of the lifted copy. Match the element's own radius; default `{ borderRadius: radius.lg }`. | StyleProp<ViewStyle> | — |
| haptic Haptics on open and while sliding across items. Default `true`. | boolean | true |
| disabled Dims the control and ignores presses. | boolean | false |
| accessibilityLabel Screen reader label of the element. | string | — |
| labels Overrides for the built-in strings; a GooduiLocaleProvider pack sets them app-wide. | Partial<ContextMenuLabels> | — |
| style Style for the outer container. | StyleProp<ViewStyle> | — |
| testID Identifier for end-to-end tests. | string | — |
Motion and haptics
- Hold: the element squeezes to 96 % over the long-press delay, so the lift reads as it popping free.
- Open: a copy of the element springs to 103 % on the bouncy spring above the dimmed backdrop (blurred on the web), and the menu grows from the edge facing the element, under its center, from 40 % scale on the bouncy spring.
- Fit: the menu sits below the element, flips above when there is no room, and when neither side fits the lifted element slides up on the smooth spring to make room.
- Slide to choose: while the finger stays down, the item under it highlights; releasing on an item runs it.
- Close: the menu fades, the copy settles back into place on the smooth spring, then the original reappears.
- Reduced motion: no squeeze, no lift scale or menu growth; the menu and backdrop fade and any shift is instant.
- Haptic: tap when the menu opens
- Haptic: select each time the finger slides onto a new item
- The press, lift, menu and slide-to-choose hit testing run on the UI thread; the menu mounts only while open.
- The lifted copy renders the children a second time while the menu is open; keep heavy children (video, maps) out of it or pass a light element.
Accessibility
- The element is one accessible element with a 'Show actions' accessibility action (longpress) and, when onPress is set, the button role and an activate action.
- The hint says 'Touch and hold for actions'; accessibilityState reports expanded while the menu is open.
- The menu has accessibilityRole menu and is modal; items are menuitems with disabled states. Opening announces the title.
- On the web the element is focusable: the ContextMenu key or Shift+F10 opens the menu, Enter or Space runs onPress, Escape closes. Android back closes.
- On the web the element is always a button with aria-haspopup menu; without onPress, Enter and Space open the menu. An accessibilityLabel must start with the element's visible text, or leave it out and let the text name it.
Notes for AI
- Children should not contain their own buttons: the element's tap goes in onPress and a nested Pressable would take the touch first.
- Set previewStyle's borderRadius to the element's own radius so the lifted copy has the same shape.
- Item onPress runs after the menu has closed. Every action needs a unique key.
- Inside a ScrollView, the hold must start before the scroll: moving the finger before the delay cancels the menu and lets the list scroll.
Related
An iOS-style list of choices that springs up from the bottom edge in grouped cards; it follows the finger to be dragged away, and pulling it up stretches the gap between the cards before they spring back together.
A bubble anchored to an element whose arrow always points at the anchor's center, even when the bubble is pushed in from the screen edge; it flips above or below to fit and unfurls out of the arrow tip.
Row actions that unfold with the finger; a long swipe lets the edge action take over and run, and destructive actions collapse the row before it leaves.