Pan gesture release triggers underlying Pressable onPress despite gesture activation
Description
When using a horizontal pan gesture for a carousel, releasing the finger after swiping triggers onPress events on underlying Pressable components, even when the pan gesture has been active and moved significantly. This does not happen with vertical scrolling (ScrollView/FlatList), which properly prevents click-through.
Environment
- React Native Gesture Handler version: 2.25.0
- React Native version: Latest (Expo SDK)
- Platform: Both iOS and Android
- Using: TypeScript, Expo Router, react-native-reanimated
Expected Behavior
When a pan gesture has been active and the user has swiped horizontally, releasing the finger should NOT trigger onPress events on underlying components, similar to how ScrollView prevents clicks after scrolling.
Actual Behavior
Horizontal swipe gesture release consistently triggers onPress on underlying Pressable components, causing unwanted navigation/actions.
Code Example
// Carousel with pan gesture
const panGesture = Gesture.Pan()
.minDistance(15)
.onBegin(() => {
startTranslateX.value = translateX.value;
})
.onUpdate((e) => {
translateX.value = startTranslateX.value + e.translationX;
})
.onEnd((e) => {
// Handle page change logic
});
// Underlying component that gets clicked incorrectly
<Pressable onPress={() => router.push('/article')}>
<ArticleCard />
</Pressable>
What We've Tried
Original attempts:
- setTimeout delays in gesture onEnd/onFinalize → App crashes immediately on swipe release
- Shared values to track gesture state → Still triggers clicks, no prevention
- minDistance + gesture activation thresholds → Still clicks through
- Press duration timing in ArticleCard (onPressIn/onPressOut) → Still triggers on release
- Various gesture configurations → Either crashes or doesn't prevent click-through
Additional attempts:
-
Tracking gesture state with
useSharedValueandpointerEvents: 'none'→ Still triggers clicks - Distance-based swipe detection with conditional blocking → Causes crashes on gesture end
- Combining minDistance with state tracking → No improvement
- Manual press handling with gesture state checks → Unreliable and still allows click-through
Comparison with ScrollView
ScrollView correctly prevents underlying touch events when scrolling occurs, but horizontal pan gestures do not exhibit the same behavior. This suggests the issue is in how pan gestures handle touch event propagation on release.
Expected Solution
Pan gestures should automatically prevent underlying touch events when:
- The gesture has been activated (moved beyond minDistance)
- The finger is released after an active gesture
- Similar to how ScrollView prevents clicks after scrolling
Additional Context
This is a common UX pattern (horizontal carousels with clickable content) and the current behavior makes it nearly impossible to implement reliably. Users expect that after swiping, they won't accidentally trigger actions on the content they swiped over.
The fact that vertical scrolling (FlatList/ScrollView) handles this correctly suggests there's an inconsistency in gesture handling between different gesture types.
Additional Technical Details:
- Using Expo Router for navigation
- TypeScript project
- NativeWind for styling
- The issue occurs regardless of whether
minDistanceis set on the pan gesture - FlatList vertical scrolling within the same interface works correctly (doesn't trigger onPress after scrolling)
Steps to Reproduce
With Expo Snack
- Open the Expo Snack
- Start a horizontal swipe gesture to change pages
- While performing the horizontal swipe, release your finger on top of one of the colored cards
-
BUG: The card's
onPressevent fires even though you were swiping, not intending to press the card
Expected behavior
When performing a horizontal swipe gesture, releasing the finger on underlying Pressable components should NOT trigger their onPress events, similar to how ScrollView behaves.
Actual behavior
The underlying Pressable components' onPress events are triggered when releasing the finger after a horizontal swipe gesture, causing unintended navigation/actions.
Additional context
- This only happens with horizontal pan gestures
- Vertical scrolling (like in ScrollView) does not have this issue
- The issue occurs on both iOS and Android
- This makes horizontal carousels with clickable content unusable
With real repo
-
Clone the repositories:
git clone https://github.com/Decryptu/CryptoastAppCache.git -b dev git clone https://github.com/Decryptu/CryptoastApp.git -b dev -
Start the cache server (dev branch):
cd CryptoastAppCache docker-compose up -
Run the Expo app (dev branch):
cd CryptoastApp npm install expo start -
Navigate to the Home tab
-
Perform a horizontal swipe gesture to change sections
-
Release finger on top of an article card
-
Bug: Article opens despite having just performed a swipe gesture
Note: Vertical scrolling (within the FlatList) correctly prevents click-through behavior.
A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug.
https://snack.expo.dev/@decryptu/pan-gesture-release-triggers
Gesture Handler version
~2.24.0
React Native version
0.79.3
Platforms
iOS
JavaScript runtime
Hermes
Workflow
Using Expo Go
Architecture
New Architecture (Fabric)
Build type
Debug mode
Device
Real device
Device model
Apple iPhone 13 Pro
Acknowledgements
Yes
Hey! 👋
It looks like you've omitted a few important sections from the issue template.
Please complete Steps to reproduce and A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug. sections.
Hey! 👋
It looks like you've omitted a few important sections from the issue template.
Please complete Steps to reproduce and A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug. sections.
Updated description with an Expo Snack.
I resolved this tap-through issue by implementing a shared gesture state that prevents underlying Pressable components from firing during swipe gestures.
Solution:
1. Global gesture state tracker:
// lib/gestureState.ts
import { makeMutable } from 'react-native-reanimated';
export const isCarouselSwiping = makeMutable(false);
2. Pan gesture sets state:
const panGesture = Gesture.Pan()
.onStart(() => {
'worklet';
isCarouselSwiping.value = true;
})
.onFinalize(() => {
'worklet';
isCarouselSwiping.value = false;
});
3. Child components check state before triggering actions:
const tapGesture = Gesture.Tap()
.onEnd((_event, success) => {
'worklet';
if (success && !isCarouselSwiping.value) {
runOnJS(onPress)();
}
});
This prevents tap events during active swipes while maintaining responsive tap behavior when not swiping. The shared value on the UI thread eliminates race conditions between gesture completion and tap detection.
Result: Clean carousel UX that matches ScrollView behavior - no accidental clicks after swiping.
Hi @Decryptu! Does this issue also happen when you use Pressable from Gesture Handler?
Hi @m-bert!
Yes, using RNGH Pressable seems to fix the issue.
Test results:
- Horizontal swipes no longer trigger onPress events on article cards
- Normal taps still work fine
- Much simpler than the custom gesture state workaround
The RNGH Pressable component properly handles gesture conflicts between pan and tap, so it prevents the tap-through behavior we were seeing with the custom tap gesture implementation.
Thanks for the suggestion, this is a cleaner solution. I will try to deploy it to my whole repo and check again if it's good on both iOS and Android.
I'm glad to hear that!
I will try to deploy it to my whole repo and check again if it's good on both iOS and Android.
Let me know if this works!
App got accepted yesterday on both store, everything is working fine 👍