Context menu

Free · MITReact Native · Expo

Long-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.

View as Markdown
Try it on your phone
Scan to open this demo in your phone's browser and use it with your fingers.

Installation

$ npx shadcn@latest add https://goodui.dev/native/r/context-menu.json

Then 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

Use it for
  • 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.
Reach for something else
  • 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

PropTypeDefault
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.
number350
menuWidth
Menu width in points. Default 250.
number250
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`.
booleantrue
disabled
Dims the control and ignores presses.
booleanfalse
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.