Skip to content

ReanimatedSwipeable: action panes are visible at rest when the row content is not fully opaque (follow-up to #4271) #4617

Closed

Description

@fregster

Environment

  • react-native-gesture-handler 3.3.0 (main 8665cd1 behaves the same)
  • React Native 0.88.0, Expo SDK 58, New Architecture
  • iOS 27, Android 17 emulator

Description

Since #4271 the left and right action containers (absoluteFill, absolutely positioned behind the row) are no longer hidden when showLeftProgress / showRightProgress is 0; only pointerEvents is toggled (src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx:432, :465). Visibility now depends entirely on the row child covering them.

If the row child is translucent (for example a 35 % opaque track or a glass-style background), both the left and right action contents show through the row while it is closed. In 2.32 the containers had opacity: show*Progress.value === 0 ? 0 : 1, so this did not happen.

Reproduction

  1. ReanimatedSwipeable with both renderLeftActions and renderRightActions (e.g. a coloured "Approve" and "Dismiss" view).
  2. Row child with a semi-transparent background (rgba(...), alpha about 0.35).
  3. At rest, with no gesture, both action contents are drawn behind/through the row.

Root cause

The opacity toggle was removed in #4271 because it flips on exact progress === 0, which hid the actions mid-way through a fast close (#3897). Nothing replaced it for the not-fully-opaque case.

Proposed fix

Hide each container unless the row is actually displaced towards it, keyed on appliedTranslation rather than on the progress value (the left actions are revealed by a positive translation, showLeftProgress = interpolate(appliedTranslation, [-1, 0, leftWidth], [0, 0, 1])):

--- a/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx
+++ b/packages/react-native-gesture-handler/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx
@@ leftActionAnimation
       pointerEvents: showLeftProgress.value === 0 ? 'none' : 'auto',
+      opacity: appliedTranslation.value > 0 ? 1 : 0,
@@ rightActionAnimation
       pointerEvents: showRightProgress.value === 0 ? 'none' : 'auto',
+      opacity: appliedTranslation.value < 0 ? 1 : 0,

Why not the 2.32 expression (opacity: show*Progress.value === 0 ? 0 : 1): that is what #4271 removed, because the progress can reach 0 (showLeftProgress.value = 0 in the close path) while the row is still sliding home, so the actions vanished mid-way through a fast close (#3897). The translation only reaches 0 when the row is actually back at rest, so a closing row keeps its pane visible until it is covered. It also keeps the opposite pane hidden during a swipe, which matters for the same translucent-row case (while swiping right, the right pane would otherwise show through the row).

We carry this as a local patch against 3.3.0; our swipe tests (which drive the real component) pass with it. We have not re-run the exact #3897 fast-close scenario on a device. Happy to open a PR if this direction suits you.


Disclosure: drafted with AI assistance (Claude) from a patch we carry locally; code references were checked against the published package sources. Happy to clarify or open a PR.

Activity

  1. github-actions commented on Oct 19, 2026

    @github-actions

    Hey @fregster! 👋

    Some of the required information in this issue is missing, incomplete or invalid. Could you take another look at the template and make sure each of these is filled in with something we can act on?

    • Description - what happens, and what you expected to happen instead
    • Steps to reproduce - steps somebody else can follow, not just numbering
    • Gesture Handler version - the exact version from your package.json or lockfile
    • React Native version - the exact version from your package.json or lockfile
    • Versions of related libraries you mention (Reanimated, Worklets, Screens, Expo) - the versions actually installed
    • Platforms - the platforms you actually see the bug on

    Once the issue is updated we will take another look. Thanks!

  2. github-actions commented on Oct 19, 2026

    @github-actions

    Hey @fregster! 👋

    The issue doesn't seem to contain a minimal reproduction.

    Could you provide a snack, a gist or a link to a GitHub repository that reproduces the problem?

  3. chocky335 commented on Oct 19, 2026

    @chocky335
    Contributor

    The fix is in #4618, together with #4616. Each action pane is hidden unless the row translation moves toward it. The panes stay hidden at rest, and the #3897 fast close keeps working.

  4. m-bert commented on Oct 20, 2026

    @m-bert
    Collaborator

    Hi @fregster! Thank you for reporting this issue!

    Thank you @chocky335 for the PR. I don't think the opacity toggle is the right approach, though. The actions still show through the translucent row while it moves, and popping into view on the first pixel of movement looks odd.

    Handling this does not require changes in the library. renderLeftActions / renderRightActions receive the row translation as a shared value, so the app can decide how its actions appear. Below you can find two examples of how to tackle this:

    • SlidingPane keeps the action attached to the row edge, so it slides in with the row and is never drawn under it.
    • FadingPane fades the action in with the reveal. It is hidden at rest, but still shows through the row while opening.

    Let me know what you think about it @fregster @chocky335.

    Example of translucent Swipeable components:
    import React from 'react';
    import type { LayoutChangeEvent } from 'react-native';
    import { StyleSheet, Text, View } from 'react-native';
    import ReanimatedSwipeable from 'react-native-gesture-handler/ReanimatedSwipeable';
    import type { SharedValue } from 'react-native-reanimated';
    import Animated, {
      Extrapolation,
      interpolate,
      useAnimatedStyle,
      useSharedValue,
    } from 'react-native-reanimated';
    
    function SlidingPane({
      side,
      translation,
      label,
    }: {
      side: 'left' | 'right';
      translation: SharedValue<number>;
      label: string;
    }) {
      const width = useSharedValue(0);
    
      const onLayout = (e: LayoutChangeEvent) => {
        width.value = e.nativeEvent.layout.width;
      };
    
      const style = useAnimatedStyle(() => ({
        transform: [
          {
            translateX:
              side === 'left'
                ? Math.min(translation.value, width.value) - width.value
                : Math.max(translation.value, -width.value) + width.value,
          },
        ],
      }));
    
      return (
        <Animated.View
          onLayout={onLayout}
          style={[
            styles.pane,
            side === 'left' ? styles.left : styles.right,
            style,
          ]}>
          <Text style={styles.paneText}>{label}</Text>
        </Animated.View>
      );
    }
    
    function FadingPane({
      side,
      translation,
      label,
    }: {
      side: 'left' | 'right';
      translation: SharedValue<number>;
      label: string;
    }) {
      const width = useSharedValue(0);
    
      const onLayout = (e: LayoutChangeEvent) => {
        width.value = e.nativeEvent.layout.width;
      };
    
      const style = useAnimatedStyle(() => {
        const reveal = side === 'left' ? translation.value : -translation.value;
        return {
          opacity:
            width.value > 0
              ? interpolate(reveal, [0, width.value], [0, 1], Extrapolation.CLAMP)
              : 0,
        };
      });
    
      return (
        <Animated.View
          onLayout={onLayout}
          style={[
            styles.pane,
            side === 'left' ? styles.left : styles.right,
            style,
          ]}>
          <Text style={styles.paneText}>{label}</Text>
        </Animated.View>
      );
    }
    
    export default function Example() {
      return (
        <View style={styles.container}>
          <ReanimatedSwipeable
            renderLeftActions={(_progress, translation) => (
              <SlidingPane side="left" translation={translation} label="Approve" />
            )}
            renderRightActions={(_progress, translation) => (
              <SlidingPane side="right" translation={translation} label="Dismiss" />
            )}>
            <View style={styles.row}>
              <Text style={styles.rowText}>sliding panes</Text>
            </View>
          </ReanimatedSwipeable>
    
          <ReanimatedSwipeable
            renderLeftActions={(_progress, translation) => (
              <FadingPane side="left" translation={translation} label="Approve" />
            )}
            renderRightActions={(_progress, translation) => (
              <FadingPane side="right" translation={translation} label="Dismiss" />
            )}>
            <View style={styles.row}>
              <Text style={styles.rowText}>fading panes</Text>
            </View>
          </ReanimatedSwipeable>
        </View>
      );
    }
    
    const styles = StyleSheet.create({
      container: { flex: 1, padding: 16, gap: 16 },
      row: {
        height: 64,
        justifyContent: 'center',
        paddingHorizontal: 16,
        backgroundColor: 'rgba(40, 40, 160, 0.35)',
      },
      rowText: { color: 'white', fontWeight: '600' },
      pane: { paddingHorizontal: 24, justifyContent: 'center' },
      left: { backgroundColor: '#2e9e4f' },
      right: { backgroundColor: '#d33' },
      paneText: { color: 'white', fontWeight: '700' },
    });
  5. chocky335 commented on Oct 20, 2026

    @chocky335
    Contributor

    Thanks, that makes sense. The translation shared value gives the app what it needs, and your SlidingPane example also avoids the pop-in that my opacity toggle had. I removed that part from #4618. If it helps, your two examples would fit the ReanimatedSwipeable docs page.

  6. m-bert commented on Oct 21, 2026

    @m-bert
    Collaborator

    @chocky335 I've slightly changed the example in the #4625, do you think it's better now? Also, I don't think FadingPane is necessary as translucent Swipeables are not that common. But if you think otherwise then let me know 😅

  7. chocky335 commented on Oct 21, 2026

    @chocky335
    Contributor

    Yes, this is better, and I agree that FadingPane is not needed. Thanks for picking it up.

  8. m-bert commented on Oct 21, 2026

    @m-bert
    Collaborator

    Thanks for confirmation! In that case I'm closing this issue.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions