CORE JSC

International Technology Partnership

React Native

Fixing React Native Gesture Conflicts Between a Swipeable Row and Its Parent ScrollView

A swipeable list row and the ScrollView it lives inside both want to own the same horizontal drag, and depending on the device, the angle of the touch, or pure luck, either one can win. The result is a list that sometimes scrolls when it should swipe, sometimes swipes when it should scroll, and is nearly impossible to reproduce reliably from a bug report alone.

Core JSC Team·September 12, 2026
React NativeGesture HandlerScrollViewTouch InputMobile UX

The Problem

A list of rows, each wrapped in a swipeable component (revealing a delete or archive action on a horizontal drag), is rendered inside a vertically scrolling ScrollView or FlatList. Users report that swiping a row sometimes scrolls the whole list instead, and scrolling the list sometimes triggers a row's swipe action instead. The bug doesn't reproduce consistently — it depends on exactly where on the row the touch starts, the angle of the drag, and sometimes the specific device, which makes it one of the harder gesture bugs to pin down from a bug report alone.

Why It Happens

A horizontal swipe and a vertical scroll are only unambiguous once a drag has traveled far enough to have a clear direction

At the instant a touch begins, the gesture system doesn't yet know whether it's the start of a horizontal swipe or a vertical scroll — both look identical for the first few pixels of movement. Two separate gesture recognizers (the swipeable row's pan gesture and the ScrollView's native scroll gesture) are both evaluating the same ambiguous initial movement, and each has to decide independently whether to claim it.

The swipeable row and the ScrollView belong to two different gesture systems by default

A ScrollView's scrolling is handled by the native scroll responder, while a swipeable row built with react-native-gesture-handler uses its own gesture recognizer system. These two systems don't automatically negotiate with each other the way two gesture-handler gestures configured as siblings would — without explicit configuration, whichever one happens to claim the touch first (often based on timing or a native-level heuristic) simply wins, regardless of which one actually matches the user's intent.

A large touch activation area on the swipeable row increases how often it wins by accident

If the swipeable row's pan gesture is configured to activate on very small movements or has a wide activation distance in both axes, it ends up capturing touches that were actually intended as a vertical scroll, simply because it reacted before the ScrollView's own gesture recognition had a chance to determine the drag was vertical.

Nested gesture handlers need an explicit relationship, not just correct individual configuration

Each gesture (the row's pan, the list's scroll) can be perfectly configured in isolation and still conflict, because the conflict isn't a configuration bug in either one — it's the absence of an explicit rule telling the gesture system how the two should behave when they compete for the same touch.

The Fix

1. Constrain the swipeable row's pan gesture to activate only on a clearly horizontal drag

import { Gesture } from "react-native-gesture-handler";

const panGesture = Gesture.Pan()
  .activeOffsetX([-10, 10]) // only activates once the drag is at least 10px horizontally
  .failOffsetY([-10, 10]);  // fails (yields to the ScrollView) once vertical movement exceeds 10px

Setting an explicit horizontal activation threshold and a vertical fail threshold gives the pan gesture a clear rule: only claim the touch once the drag has demonstrably gone sideways, and immediately give up as soon as it's clear the drag is actually vertical — removing the ambiguity that let it win scroll gestures by accident.

2. Use simultaneousHandlers or a native ScrollView reference when both gestures should be allowed to coexist

import { GestureDetector } from "react-native-gesture-handler";
import { ScrollView } from "react-native-gesture-handler";
import { useRef } from "react";

function SwipeableList({ children }) {
  const scrollRef = useRef(null);
  const panGesture = Gesture.Pan()
    .activeOffsetX([-10, 10])
    .failOffsetY([-10, 10])
    .simultaneousWithExternalGesture(scrollRef);

  return (
    
      {children}
    
  );
}

Using the gesture-handler-provided ScrollView (rather than the core React Native one) alongside simultaneousWithExternalGesture puts both gestures under the same recognition system, letting them negotiate directly instead of one belonging to native scroll handling and the other to an unrelated recognizer.

3. Reduce the row's activation sensitivity so accidental micro-drags don't trigger a swipe

const panGesture = Gesture.Pan()
  .activeOffsetX([-15, 15])
  .minDistance(10); // ignore movement below this threshold entirely

Widening the horizontal activation offset and setting a minimum distance means a touch that starts as a slight, ambiguous wobble — common when a user's finger isn't moving in a perfectly straight line — doesn't get interpreted as a deliberate horizontal swipe before the direction is actually clear.

4. Test on a real device with a low-friction touch surface, not only the simulator

npx react-native run-android --variant=release
# or
npx react-native run-ios --configuration Release

Simulator touch input is mouse-driven and doesn't reproduce the subtle diagonal drift a real finger produces on glass, which is often exactly the input pattern that exposes gesture-priority bugs. Confirming the fix on a physical device catches cases that look resolved in the simulator but still misfire on real hardware.

Why This Works

Each fix resolves the same underlying issue — two gesture recognizers evaluating the same ambiguous initial touch — through a different, complementary mechanism. Explicit activation and fail offsets give the pan gesture a concrete rule for when a drag is unambiguously horizontal versus vertical; using the gesture-handler-aware ScrollView with simultaneous-handler configuration puts both gestures in the same negotiation system instead of two unrelated ones; widening the activation threshold filters out the small, direction-ambiguous movements that cause false positives; and real-device testing verifies the fix against the actual touch input pattern that exposed the bug in the first place, not an approximation of it.

Conclusion

A swipeable row fighting its parent ScrollView for the same touch isn't a bug in either component individually — it's the lack of an explicit rule for how two separate gesture recognizers should resolve an ambiguous initial drag. Constrain the swipe gesture with horizontal activation and vertical fail offsets, put both gestures under the same recognition system with a gesture-handler ScrollView and simultaneous handlers, tune activation sensitivity to filter out accidental micro-drags, and confirm the fix on real hardware where the ambiguous touch patterns that caused the bug actually occur.