Description
When using <MenuView shouldOpenOnLongPress> on iOS 18 (and likely iOS 15–17) with React Native's New Architecture (Fabric), the context menu preview shows a blank white/colored rectangle with no content. The message text, background gradients, and custom border radii are all missing from the preview. Only the menu action items render correctly.
This does not occur on:
- iOS 26 (LiquidGlass) — preview renders correctly
- Android — no preview mechanism, works fine
shouldOpenOnLongPress={false} (tap-to-open) — no preview animation, works fine
Environment
- React Native: 0.79 (New Architecture / Fabric enabled)
- @react-native-menu/menu: 2.0.0
- iOS: 18.x (tested on Simulator, iPhone 16 Pro)
- Xcode: 26.x
- Architecture: Fabric (bridgeless)
Steps to Reproduce
<MenuView
actions={[
{ id: 'reply', title: 'Reply', image: 'arrowshape.turn.up.left' },
{ id: 'copy', title: 'Copy', image: 'doc.on.doc' },
]}
onPressAction={({ nativeEvent }) => console.log(nativeEvent.event)}
shouldOpenOnLongPress
>
<View style={{
backgroundColor: '#007AFF',
borderRadius: 16,
padding: 12,
}}>
<Text style={{ color: 'white' }}>Hello, this is a message bubble</Text>
</View>
</MenuView>
- Long press the view on iOS 18 Simulator or device
- Observe: a blank white rectangle lifts up, then transitions to a rounded-corner blank shape as the menu appears
- Expected: the blue bubble with "Hello, this is a message bubble" text should be visible in the preview
Root Cause Analysis
Why tap-to-open works but long-press doesn't
The library has two distinct menu presentation paths:
Path 1 — Tap (shouldOpenOnLongPress = false):
// In setup():
self.menu = menu
self.showsMenuAsPrimaryAction = true
UIButton presents the menu immediately on tap — there is no "lift" preview animation. No snapshotting occurs. This works perfectly.
Path 2 — Long press (shouldOpenOnLongPress = true):
// In init:
let interaction = UIContextMenuInteraction(delegate: self)
self.addInteraction(interaction)
// In setup():
self.menu = nil // UIButton's built-in menu is disabled
self.showsMenuAsPrimaryAction = false
The UIContextMenuInteraction delegate handles the menu. iOS always performs a "lift + preview" animation, which requires snapshotting the source view via CALayer rendering.
Why the snapshot is blank
React Native's Fabric renderer uses Metal-backed CALayer sublayers for text (RCTTextLayer) and custom shapes. These layers are populated asynchronously via Metal GPU textures, not through the standard CALayer backing store.
iOS 18's UITargetedPreview internally uses CALayer snapshotting to capture the preview image. This snapshotting mechanism cannot read Metal texture content — it sees the layers as empty/transparent. The result is a blank rectangle with only the view's backgroundColor visible (if any).
iOS 26 (LiquidGlass) appears to have fixed this in Apple's new rendering pipeline, which is why the preview works there.
Attempted Fixes (All Failed)
Attempt 1: CALayer.render(in:) manual snapshot
public override func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
let reactView = self.subviews.first(where: { NSStringFromClass(type(of: $0)) == "RCTView" })
guard let view = reactView ?? self.subviews.last else { return nil }
UIGraphicsBeginImageContextWithOptions(view.bounds.size, false, 0.0)
defer { UIGraphicsEndImageContext() }
guard let context = UIGraphicsGetCurrentContext() else { return nil }
view.layer.render(in: context)
guard let image = UIGraphicsGetImageFromCurrentImageContext() else { return nil }
let imageView = UIImageView(image: image)
let params = UIPreviewParameters()
params.backgroundColor = .clear
let target = UIPreviewTarget(container: view, center: CGPoint(x: view.bounds.midX, y: view.bounds.midY))
return UITargetedPreview(view: imageView, parameters: params, target: target)
}
Result: Blank image. CALayer.render(in:) cannot capture Metal-backed sublayers. The rendered UIImage contains only the background color with no text or custom shapes.
Attempt 2: drawHierarchy(in:afterScreenUpdates:)
viewToSnapshot.drawHierarchy(in: bounds, afterScreenUpdates: true)
Result: Still blank. drawHierarchy is supposed to capture on-screen content, but on iOS 18 with Fabric views, the Metal textures are still not committed to the standard rendering pipeline when this is called during the context menu interaction.
Attempt 3: Target the inner RCTView directly (no snapshot)
let reactView = self.subviews.first(where: { NSStringFromClass(type(of: $0)) == "RCTView" })
guard let view = reactView else { return nil }
let params = UIPreviewParameters()
params.backgroundColor = .clear
return UITargetedPreview(view: view, parameters: params)
Result: Blank. iOS's internal UITargetedPreview rendering also uses CALayer snapshotting internally, hitting the same Metal texture limitation.
Attempt 4: Suppress preview via visiblePath
let params = UIPreviewParameters()
params.backgroundColor = .clear
params.visiblePath = UIBezierPath(rect: CGRect(x: 0, y: 0, width: 0.01, height: 0.01))
return UITargetedPreview(view: sourceView, parameters: params)
Result: iOS 18 ignores the near-zero visiblePath and still renders the full blank rectangle with the system's default rounded-corner mask.
Attempt 5: Return nil from preview delegates
public override func contextMenuInteraction(
_ interaction: UIContextMenuInteraction,
previewForHighlightingMenuWithConfiguration configuration: UIContextMenuConfiguration
) -> UITargetedPreview? {
return nil // "Skip" the preview
}
Result: nil does NOT mean "no preview." Per Apple's documentation, returning nil means "use the default preview" — which snapshots the source view, producing the same blank rectangle.
Attempt 6: Use self (the UIButton) as preview target
let params = UIPreviewParameters()
params.backgroundColor = .clear
params.visiblePath = UIBezierPath()
return UITargetedPreview(view: self, parameters: params)
Result: Crash — NSInternalInconsistencyException: Attempting to create a UIPreviewTarget with an invalid location: {nan, nan, 0}. The MenuViewImplementation UIButton has {NaN, NaN} coordinates because React Native Fabric doesn't assign it a valid frame (the child RCTView visually overflows from the 0×0 button).
Key Finding
There is no public UIKit API to disable UIContextMenuInteraction's preview animation. The preview is a mandatory part of the interaction. The only documented ways to customize it (previewForHighlightingMenuWithConfiguration, previewProvider, UIPreviewParameters.visiblePath) all still require a valid view snapshot, which Fabric cannot provide on iOS 18.
Suggested Fix for the Library
Option A: Transparent overlay approach
Attach the UIContextMenuInteraction to an empty transparent UIView overlay instead of the content view itself. The preview animation will "lift" the invisible overlay while the actual content stays in place underneath. The menu still appears anchored correctly.
// In init:
let overlay = UIView()
overlay.backgroundColor = .clear
self.addSubview(overlay)
let interaction = UIContextMenuInteraction(delegate: self)
overlay.addInteraction(interaction) // interaction on overlay, not self
Option B: Expose a previewDisabled prop
Allow consumers to opt out of the preview entirely by returning a minimal UITargetedPreview or by not adding the UIContextMenuInteraction and instead using UIButton.menu with showsMenuAsPrimaryAction = false (which shows a menu on long press via UIButton's internal mechanism — still has a preview, but it's handled by UIButton's private optimized APIs which may work better).
Option C: iOS version gating in the library
Conditionally skip the UIContextMenuInteraction for shouldOpenOnLongPress on iOS < 26, and fall back to UIButton.menu with showsMenuAsPrimaryAction = false.
Description
When using
<MenuView shouldOpenOnLongPress>on iOS 18 (and likely iOS 15–17) with React Native's New Architecture (Fabric), the context menu preview shows a blank white/colored rectangle with no content. The message text, background gradients, and custom border radii are all missing from the preview. Only the menu action items render correctly.This does not occur on:
shouldOpenOnLongPress={false}(tap-to-open) — no preview animation, works fineEnvironment
Steps to Reproduce
Root Cause Analysis
Why tap-to-open works but long-press doesn't
The library has two distinct menu presentation paths:
Path 1 — Tap (
shouldOpenOnLongPress = false):UIButton presents the menu immediately on tap — there is no "lift" preview animation. No snapshotting occurs. This works perfectly.
Path 2 — Long press (
shouldOpenOnLongPress = true):The
UIContextMenuInteractiondelegate handles the menu. iOS always performs a "lift + preview" animation, which requires snapshotting the source view viaCALayerrendering.Why the snapshot is blank
React Native's Fabric renderer uses Metal-backed
CALayersublayers for text (RCTTextLayer) and custom shapes. These layers are populated asynchronously via Metal GPU textures, not through the standardCALayerbacking store.iOS 18's
UITargetedPreviewinternally usesCALayersnapshotting to capture the preview image. This snapshotting mechanism cannot read Metal texture content — it sees the layers as empty/transparent. The result is a blank rectangle with only the view'sbackgroundColorvisible (if any).iOS 26 (LiquidGlass) appears to have fixed this in Apple's new rendering pipeline, which is why the preview works there.
Attempted Fixes (All Failed)
Attempt 1:
CALayer.render(in:)manual snapshotResult: Blank image.
CALayer.render(in:)cannot capture Metal-backed sublayers. The renderedUIImagecontains only the background color with no text or custom shapes.Attempt 2:
drawHierarchy(in:afterScreenUpdates:)Result: Still blank.
drawHierarchyis supposed to capture on-screen content, but on iOS 18 with Fabric views, the Metal textures are still not committed to the standard rendering pipeline when this is called during the context menu interaction.Attempt 3: Target the inner
RCTViewdirectly (no snapshot)Result: Blank. iOS's internal
UITargetedPreviewrendering also usesCALayersnapshotting internally, hitting the same Metal texture limitation.Attempt 4: Suppress preview via
visiblePathResult: iOS 18 ignores the near-zero
visiblePathand still renders the full blank rectangle with the system's default rounded-corner mask.Attempt 5: Return
nilfrom preview delegatesResult:
nildoes NOT mean "no preview." Per Apple's documentation, returningnilmeans "use the default preview" — which snapshots the source view, producing the same blank rectangle.Attempt 6: Use
self(the UIButton) as preview targetResult: Crash —
NSInternalInconsistencyException: Attempting to create a UIPreviewTarget with an invalid location: {nan, nan, 0}. TheMenuViewImplementationUIButton has{NaN, NaN}coordinates because React Native Fabric doesn't assign it a valid frame (the childRCTViewvisually overflows from the 0×0 button).Key Finding
There is no public UIKit API to disable
UIContextMenuInteraction's preview animation. The preview is a mandatory part of the interaction. The only documented ways to customize it (previewForHighlightingMenuWithConfiguration,previewProvider,UIPreviewParameters.visiblePath) all still require a valid view snapshot, which Fabric cannot provide on iOS 18.Suggested Fix for the Library
Option A: Transparent overlay approach
Attach the
UIContextMenuInteractionto an empty transparentUIViewoverlay instead of the content view itself. The preview animation will "lift" the invisible overlay while the actual content stays in place underneath. The menu still appears anchored correctly.Option B: Expose a
previewDisabledpropAllow consumers to opt out of the preview entirely by returning a minimal
UITargetedPreviewor by not adding theUIContextMenuInteractionand instead usingUIButton.menuwithshowsMenuAsPrimaryAction = false(which shows a menu on long press via UIButton's internal mechanism — still has a preview, but it's handled by UIButton's private optimized APIs which may work better).Option C: iOS version gating in the library
Conditionally skip the
UIContextMenuInteractionforshouldOpenOnLongPresson iOS < 26, and fall back toUIButton.menuwithshowsMenuAsPrimaryAction = false.