React Native Context Menu: Long Press and Preview
By Native Animation · Updated
A React Native context menu shows commands for a specific item, usually after a long press or an explicit actions button. A preview keeps the target recognizable while people choose a command. Offer a tap and screen-reader path too, and keep opening, highlighting, and executing an action separate so that exploring a menu does not mutate the item.
This guide focuses on invocation, preview, and action ownership. Use the React Native overlays guide for anchor measurement, placement, clipping, back handling, and focus containment; those presentation mechanics belong in one place.
Choose contextual actions, not form selection
| Interaction | Meaning | Related guide |
|---|---|---|
| Context menu on a photo or row | Act on that particular item | This guide |
| Dropdown or multi-select | Store one or more form values | Dropdown and select |
| Tooltip | Explain a control | Touch help |
| Popover or sheet | Present temporary content above the screen | Overlays |
A context menu describes the relationship between an item and its commands; an overlay describes where the surface renders. The same command set can use a compact popover on a tablet or a larger preview on a phone. A custom cross-platform menu does not become the operating system's native context-menu API because it resembles one.
Long press and a visible button
Use long press as a shortcut, with a visible actions button as a discoverable alternative. Both paths should capture the same stable target before opening. The React Native 0.88 Pressable documentation describes onLongPress and delayLongPress for a discrete hold. Do not execute a destructive command in onPressIn or onLongPress: those events request the menu, not a deletion.
If the row's regular tap opens details, keep the actions button separate from that primary operation. Coordinate gesture recognition with scrolling so dragging a list does not accidentally open actions. For a continuous hold-and-scrub menu, use one gesture session that owns the preview and highlight; do not bolt an independent long-press handler onto the same component's gesture detector.
Preview before a command
Capture the source item's ID and the preview data at invocation. A highlighted action is only a candidate; releasing over it or explicitly activating its row requests execution. Moving away or canceling should leave the item unchanged. If an item disappears during an open menu, dismiss or resolve its command against the captured ID rather than whatever now occupies that array index.
Use a local or already-loaded preview where possible so opening does not reveal a blank image. Reserve its geometry while loading remote content and show a fallback on failure. Keep the preview associated with the source without obscuring essential action labels. A visual shrinking or blurring effect alone is not a focus boundary.
Integrate Peek Context Menu
Peek Context Menu supplies its own photo gallery, measured source-aware preview, fixed action rows, and hold-to-scrub interaction. Its public ref exposes open(index), close(), and play(index). Pass an explicit index: open() has a demo-oriented default. It is not a generic wrapper around arbitrary children, and it has no public actions prop for replacing the menu.
Copy the piece using the CLI, then install its dependencies with Expo:
npx expo install @shopify/react-native-skia expo-blur expo-image expo-linear-gradient react-native-gesture-handler react-native-reanimated react-native-workletsUse bunx for Bun projects. Target Expo SDK 58, React Native 0.88.0, Reanimated 4.7.1, Worklets 0.13.0, and Gesture Handler 3.3. Follow the SDK 58 Reanimated, Gesture Handler, and Skia setup. Mount inside GestureHandlerRootView in a safe-area-bounded screen. Web requires CanvasKit initialization before importing the component.
The complete wrapper below takes photos as props, adds a button to open the first photo's actions, and translates the component's index into the host's stable photo ID. Supply your own local image assets and action handler when rendering it. Keep the photo array fixed while a preview is open; reconcile live list updates after closing. This wrapper delegates command handling to the host and does not perform sharing, copying, editing, or deletion.
import { useRef } from 'react';
import { Button, View, type ImageSourcePropType } from 'react-native';
import {
PeekContextMenu, type PeekContextMenuActionId, type PeekContextMenuRef,
} from './peek-context-menu';
type Photo = { id: string; source: ImageSourcePropType };
type PhotoActionsProps = {
photos: Photo[];
onRequestAction: (action: PeekContextMenuActionId, photoId: string) => void;
};
export function PhotoActions({ photos, onRequestAction }: PhotoActionsProps) {
const menu = useRef<PeekContextMenuRef>(null);
return (
<View style={{ flex: 1 }}>
<Button title="Actions for first photo" disabled={photos.length === 0}
onPress={() => menu.current?.open(0)} />
<PeekContextMenu ref={menu} images={photos.map((photo) => photo.source)}
brandName="Field Notes" title="Photos"
itemCountLabel={`${photos.length} photos`}
onAction={(action, index) => {
const photo = photos[index];
if (photo) onRequestAction(action, photo.id);
}}
style={{ flex: 1 }} />
</View>
);
}Treat callbacks as requests, not success receipts
The action IDs are share, favorite, copy, edit, and delete. Your host implements those operations and handles failure. For delete, request confirmation or an undoable mutation before making a permanent change. For favorite, update your records and supply favoriteIndices derived from them if you want the gallery badges to reflect accepted state. No photo storage or clipboard implementation comes with the animation.
The current source shows a built-in success-style toast immediately after onAction and starts closing. That toast is visual demo feedback, not confirmation from your data layer, and there is no public success/error feedback prop. Adapt the copied source to wait for accepted results and reflect failure before using it for asynchronous commands. If your handler opens another modal, queue that presentation until the preview has finished closing; onAction itself is not an after-close callback.
Accessibility and overlay integration
The gallery's labels, named Show actions accessibility activation, tap gesture, and labeled menu rows provide alternatives to holding and scrubbing. Reduced Motion uses short fades instead of spring travel. These features follow the React Native accessibility model; they still need validation in the final screen.
The component has an inline overlay and a close backdrop, but its public API has no open/onOpenChange or onRestoreFocus contract. The example above demonstrates invocation and action mapping, not a complete modal focus policy. For a production integration, adapt the copied source to expose visibility and a close-completion notification. Use them to hide background siblings, move focus into the actions, restore it to a surviving trigger, and handle Android back or web Escape at the correct presentation boundary. Follow the overlay guide's focus and dismissal implementation rather than adding an untracked backdrop around this gallery.
Keep preview animation, backdrop, and accessibility blocking alive until exit completes. Do not claim a menu role while omitting the matching keyboard interaction: labeled buttons in a named dialog can be a clearer contract for a custom preview. Web keyboard listeners do not automatically implement native hardware-keyboard navigation. Native accessibility focus and input focus are separate concerns.
If the selected photo is deleted, return focus to the next surviving photo or the gallery heading. A detached trigger cannot receive restored focus. Keep Cancel or Close reachable with large text and a short screen, and ensure destructive meaning has visible text rather than color alone.
Test interrupted interactions
Check a short tap, a long press while the gallery is scrolled, release over an action, release outside actions, and explicit row activation. Verify that each chosen command reaches the host once and that merely opening or moving the highlight does not commit a command. Then interrupt closing with another open and dismiss during measurement; the component guards stale measurements and closing completions, but the host must still guard stale data requests.
Test the first and last thumbnail, an empty gallery, a remote-image failure, large text, Reduce Motion, screen-reader entry and exit, Android back, and web Escape. The piece measures in window coordinates and subtracts the overlay origin so a scrolled thumbnail can return to its source. A different native presentation or clipped ancestor still needs the overlay placement rules.
Each published piece is reviewed on physical iPhone and Android devices with Expo Go before release. Your app's command persistence, confirmation, navigation, and focus integration remain host responsibilities. Browse micro-interactions for related controls.
FAQ
Is long press required for a React Native context menu?
No. Offer a visible actions button and a screen-reader activation path. Peek Context Menu also provides tap opening and the open(index) ref method.
Does this use the native iOS context-menu API?
No. It is a custom React Native gallery and animated preview using Expo-compatible dependencies. Native platform menu APIs have their own presentation and interaction contracts.
Can I pass custom actions or arbitrary row children?
The current piece exposes a fixed photo gallery and five fixed action IDs. It does not expose actions or children props for a generic menu. Adapt the copied source or use the anchored popover pattern for a different command set.
Does selecting Delete remove the photo?
It calls onAction with delete and the photo index. The host owns confirmation, persistence, failure, and any undo behavior. The built-in toast does not prove that deletion succeeded.