react-native-gesture-handler icon indicating copy to clipboard operation
react-native-gesture-handler copied to clipboard

Pan gesture release triggers underlying Pressable onPress despite gesture activation

Open Decryptu opened this issue 8 months ago • 3 comments

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:

  1. setTimeout delays in gesture onEnd/onFinalize → App crashes immediately on swipe release
  2. Shared values to track gesture state → Still triggers clicks, no prevention
  3. minDistance + gesture activation thresholds → Still clicks through
  4. Press duration timing in ArticleCard (onPressIn/onPressOut) → Still triggers on release
  5. Various gesture configurations → Either crashes or doesn't prevent click-through

Additional attempts:

  1. Tracking gesture state with useSharedValue and pointerEvents: 'none' → Still triggers clicks
  2. Distance-based swipe detection with conditional blocking → Causes crashes on gesture end
  3. Combining minDistance with state tracking → No improvement
  4. 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:

  1. The gesture has been activated (moved beyond minDistance)
  2. The finger is released after an active gesture
  3. 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 minDistance is 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

  1. Open the Expo Snack
  2. Start a horizontal swipe gesture to change pages
  3. While performing the horizontal swipe, release your finger on top of one of the colored cards
  4. BUG: The card's onPress event 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

  1. Clone the repositories:

    git clone https://github.com/Decryptu/CryptoastAppCache.git -b dev
    git clone https://github.com/Decryptu/CryptoastApp.git -b dev
    
  2. Start the cache server (dev branch):

    cd CryptoastAppCache
    docker-compose up
    
  3. Run the Expo app (dev branch):

    cd CryptoastApp
    npm install
    expo start
    
  4. Navigate to the Home tab

  5. Perform a horizontal swipe gesture to change sections

  6. Release finger on top of an article card

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

Decryptu avatar Jun 13 '25 13:06 Decryptu

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.

github-actions[bot] avatar Jun 13 '25 13:06 github-actions[bot]

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.

Decryptu avatar Jun 13 '25 14:06 Decryptu

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.

Decryptu avatar Jun 24 '25 18:06 Decryptu

Hi @Decryptu! Does this issue also happen when you use Pressable from Gesture Handler?

m-bert avatar Jul 03 '25 10:07 m-bert

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.

Decryptu avatar Jul 03 '25 12:07 Decryptu

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!

m-bert avatar Jul 03 '25 12:07 m-bert

App got accepted yesterday on both store, everything is working fine 👍

Decryptu avatar Jul 08 '25 09:07 Decryptu