Skip to content

ReanimatedSwipeable: allow passing failOffsetY to the internal pan gesture #4563

Closed

Description

@willheisenberg

Description

ReanimatedSwipeable builds its pan gesture internally with only activeOffsetX: [dragOffsetFromRight, dragOffsetFromLeft]. There is no way to pass failOffsetY (or other pan config) to that gesture.

On Android this breaks pull-to-refresh when swipeable rows are rendered inside a FlatList with a RefreshControl:

  1. A short, slightly diagonal downward drag on a row first starts the native pull-to-refresh (SwipeRefreshLayout).
  2. The horizontal component of the same drag then crosses activeOffsetX, the swipe pan activates and Gesture Handler cancels the native touches (ACTION_CANCEL).
  3. AndroidX SwipeRefreshLayout only retracts its indicator on ACTION_UP, not on ACTION_CANCEL. The indicator stays parked half-pulled, without spinning and without onRefresh firing, until the next pull picks it up again.

It does not happen when dragging outside the rows or on purely vertical drags.

Expected: a mostly vertical drag that has already started pull-to-refresh should not be taken over by the row swipe.

Proposed solution

An optional failOffsetY prop on ReanimatedSwipeable, forwarded unchanged to the internal pan gesture. Without the prop nothing changes. Setting it to roughly the Android touch slop (e.g. [-8, 8]) makes the swipe fail as soon as the finger moves vertically first, so it no longer takes over a pull that has started, while horizontal swiping keeps working.

We have been running this as a local patch and verified it on device: after the change, 10 out of 10 light (including diagonal) pulls no longer left the indicator stuck. I'll open a PR with the change.

Steps to reproduce

  1. Render a FlatList with refreshing / onRefresh where every row is wrapped in ReanimatedSwipeable with renderLeftActions (snippet below).
  2. Run on an Android device.
  3. At the top of the list, drag on a row briefly and slightly diagonally downwards (about 70 px down and 100 px sideways, not past the refresh threshold), then release.
  4. The refresh indicator stays half-pulled and does not spin; onRefresh is not called.
import React, { useCallback, useState } from 'react';
import { FlatList, RefreshControl, Text, View } from 'react-native';
import ReanimatedSwipeable from 'react-native-gesture-handler/ReanimatedSwipeable';

const DATA = Array.from({ length: 30 }, (_, i) => `Row ${i}`);

export default function App() {
  const [refreshing, setRefreshing] = useState(false);
  const onRefresh = useCallback(() => {
    setRefreshing(true);
    setTimeout(() => setRefreshing(false), 1000);
  }, []);

  return (
    <FlatList
      data={DATA}
      keyExtractor={(item) => item}
      refreshControl={
        <RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
      }
      renderItem={({ item }) => (
        <ReanimatedSwipeable
          // failOffsetY={[-8, 8]} // proposed prop
          renderLeftActions={() => (
            <View style={{ width: 80, backgroundColor: 'tomato' }} />
          )}>
          <View style={{ padding: 20, backgroundColor: 'white' }}>
            <Text>{item}</Text>
          </View>
        </ReanimatedSwipeable>
      )}
    />
  );
}

A link to a Gist, an Expo Snack or a link to a repository based on this template that reproduces the bug.

See the code snippet above.

Gesture Handler version

3.3.0

React Native version

0.87

Platforms

Android

JavaScript runtime

Hermes

Workflow

No response

Build type

No response

Device

Real device

Device model

Samsung tablet (Android 15 / Android 16)

Acknowledgements

Yes

Activity

  1. m-bert commented on Oct 13, 2026

    @m-bert
    Collaborator

    Hi @willheisenberg! Thank you for reporting this issue! This should be fixed by #4582 and #4592. It would be great if you could confirm that applying #4592 works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions