Shipnative
ShipnativeShipnative
Sign in
GuideSeptember 2026 · 9 min read

React Native Swipe to Delete: Swipeable Rows (2026)

Swipe-to-delete looks like a small feature and behaves like a medium one. The gesture itself is handled for you by Swipeable, but the things that make it feel native — an action that animates in with the drag, only one row open at a time, and a delete that’s instant but reversible — are all yours to build. Here’s the whole thing, including the silent setup failure that makes the gesture do nothing at all.

Setup — and the one line everything depends on

npx expo install react-native-gesture-handler react-native-reanimated
// app/_layout.tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { Stack } from 'expo-router';

export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Stack />
    </GestureHandlerRootView>
  );
}

If the swipe does nothing — no movement, no error, no warning — this is why, about nine times out of ten. And style={{ flex: 1 }} is not decorative: without it the root view collapses to zero height and your entire app renders blank, which is a different and briefly terrifying bug.

A row that swipes

import { Pressable, StyleSheet, Text, View } from 'react-native';
import Swipeable from 'react-native-gesture-handler/ReanimatedSwipeable';
import Reanimated, { SharedValue, useAnimatedStyle } from 'react-native-reanimated';

function DeleteAction({
  drag, onPress,
}: { drag: SharedValue<number>; onPress: () => void }) {
  // drag is negative as the row moves left; +80 puts the action back at rest
  const style = useAnimatedStyle(() => ({
    transform: [{ translateX: drag.value + 80 }],
  }));

  return (
    <Reanimated.View style={[styles.actionWrap, style]}>
      <Pressable style={styles.action} onPress={onPress} accessibilityLabel="Delete">
        <Text style={styles.actionText}>Delete</Text>
      </Pressable>
    </Reanimated.View>
  );
}

export function TaskRow({ task, onDelete }) {
  return (
    <Swipeable
      friction={2}                  // 1 feels twitchy; 2 tracks the thumb
      rightThreshold={40}           // how far before it snaps open
      overshootRight={false}        // no rubber-band past the action
      renderRightActions={(_progress, drag) => (
        <DeleteAction drag={drag} onPress={() => onDelete(task.id)} />
      )}
    >
      <View style={styles.row}>
        <Text style={styles.title}>{task.title}</Text>
      </View>
    </Swipeable>
  );
}

const styles = StyleSheet.create({
  row: { backgroundColor: '#1c1c1c', padding: 16, minHeight: 60, justifyContent: 'center' },
  title: { color: 'rgba(255,255,255,0.85)', fontSize: 15 },
  actionWrap: { width: 80 },
  action: { flex: 1, backgroundColor: '#dc2626', alignItems: 'center', justifyContent: 'center' },
  actionText: { color: '#fff', fontWeight: '600' },
});

Two things worth noticing. The row has an opaque background — a transparent row lets the red action show through underneath at rest, which looks like a rendering bug. And renderRightActions gets a live drag shared value, so the action tracks the finger on the UI thread rather than appearing in one jump when the threshold is crossed. That difference is most of what makes it feel native.

Only one row open at a time

Swipeable knows nothing about its siblings, so by default a user can leave four rows half-open and the list looks wrecked. Every polished implementation tracks the open row at the list level:

export default function TaskList({ tasks, onDelete }) {
  const openRef = useRef<SwipeableMethods | null>(null);

  const registerOpen = useCallback((row: SwipeableMethods | null) => {
    if (openRef.current && openRef.current !== row) {
      openRef.current.close();
    }
    openRef.current = row;
  }, []);

  return (
    <FlatList
      data={tasks}
      keyExtractor={(t) => t.id}
      renderItem={({ item }) => (
        <TaskRow task={item} onDelete={onDelete} onOpen={registerOpen} />
      )}
      // closing on scroll is what iOS Mail does
      onScrollBeginDrag={() => { openRef.current?.close(); openRef.current = null; }}
    />
  );
}

// inside TaskRow:
const rowRef = useRef<SwipeableMethods>(null);
<Swipeable
  ref={rowRef}
  onSwipeableWillOpen={() => onOpen(rowRef.current)}
  onSwipeableWillClose={() => onOpen(null)}
  ...
/>

Closing on scroll is the detail that separates this from a demo. Nobody wants to scroll a list with an open red panel following them down the screen.

Delete instantly, offer undo

The instinct is to confirm. Resist it. A modal after a swipe cancels the speed the gesture existed to provide, and people tap through confirmations without reading them anyway — so it costs the common case and barely protects the rare one. Remove the row immediately, hold the delete for a few seconds, and let the user take it back.

const pending = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());

const deleteTask = useCallback((id: string) => {
  const removed = tasks.find((t) => t.id === id);
  setTasks((prev) => prev.filter((t) => t.id !== id));   // gone from the UI now

  const timer = setTimeout(() => {
    pending.current.delete(id);
    api.deleteTask(id).catch(() => {
      setTasks((prev) => [...prev, removed]);            // server said no — put it back
      toast('Could not delete. Restored.');
    });
  }, 5000);

  pending.current.set(id, timer);

  toast('Task deleted', {
    action: {
      label: 'Undo',
      onPress: () => {
        clearTimeout(pending.current.get(id));
        pending.current.delete(id);
        setTasks((prev) => [...prev, removed]);
      },
    },
  });
}, [tasks]);

// flush anything still pending if the screen unmounts
useEffect(() => () => {
  pending.current.forEach((timer) => clearTimeout(timer));
}, []);

The cleanup matters. If the user navigates away inside the five seconds, that timer either fires against an unmounted component or the delete silently never happens, depending on how you wrote it. Decide which behaviour you want — most apps should commit the delete on unmount rather than drop it — and write it down explicitly instead of leaving it to timing.

The traps, in the order you’ll hit them

  1. Nothing swipes. No GestureHandlerRootView, or it isn’t at the true root.
  2. The action shows through the row. The row has no background colour, so the red panel is visible underneath at rest.
  3. Swipe fights the vertical scroll. Raise friction and rightThreshold — a hair-trigger row opens every time someone scrolls with a slightly diagonal thumb.
  4. Wrong row deleted after scrolling. You captured the index instead of the id, and virtualization reused the row component for different data. Always close over the id.
  5. The row stays open after delete. Call close()before removing the item, or the exit animation runs on a row that’s already gone.
  6. It works on iOS, feels wrong on Android. Swipe-to-delete is an iOS idiom; Android users often expect long-press-to-select instead. On Android, keep the swipe but add a long-press selection mode rather than relying on the gesture alone.

One accessibility note: a swipe is invisible to a screen reader. Add an accessibilityActionfor delete on the row, or the feature simply doesn’t exist for those users.

The shortcut: generate the wiring, keep the taste

The gesture config, the open-row bookkeeping, the optimistic-delete timer — none of it is a product decision. The product decisions are how long undo lasts and whether a delete is recoverable at all, and the only way to judge the gesture itself is with a thumb on a real screen.

Describe the screen in ShipNative — “a task list where swiping a row left deletes it with an undo toast” — and it wires the gesture root, the swipeable rows, the animated action and the undo, then runs it on your phone. Adjust the feel by prompting; export the full Expo project whenever you want it.

Build it free

Describe your app in one sentence and have it running on your phone in minutes at shipnative.dev. No credit card, full React Native source export whenever you want it.

Frequently Asked Questions

How do I add swipe to delete in React Native?

Wrap each list row in the Swipeable component from react-native-gesture-handler, and pass renderRightActions to draw the action revealed by the swipe. You also need react-native-reanimated installed and GestureHandlerRootView at the root of the app, or the gesture never reaches the row.

Why is my swipeable row not swiping?

The overwhelmingly common cause is a missing GestureHandlerRootView at the true root of the app — without it no gesture handler receives touches, and the failure is silent. After that, check that the row is not inside a horizontal ScrollView that is claiming the same axis, and that you imported Swipeable from react-native-gesture-handler rather than an abandoned third-party package.

Should swiping delete immediately or ask for confirmation?

Delete immediately and offer undo. A confirmation dialog after a swipe undoes the speed the gesture exists to provide, and users still tap Confirm without reading. Removing the row instantly with a five-second undo toast is faster for the common case and safer for the mistake, because it does not depend on the user reading anything.

How do I make only one row open at a time?

Keep a ref to the currently open Swipeable in the list component. In each row onSwipeableWillOpen, close the previously stored ref and store this one. Without it, users end up with three half-open rows and a list that looks broken, because Swipeable has no awareness of its siblings.

Does swipe to delete work with FlashList?

Yes — Swipeable is just a wrapper around each row, so it works with FlatList, SectionList, and FlashList alike. With FlashList, be more careful about cleaning up the open-row ref on recycle, because the underlying row component gets reused for different data as you scroll.

→

React Native Gesture Handler

The gesture system underneath — pan, tap, and the root view rule.

Read guide →
→

React Native Toast Messages

The undo affordance that makes instant delete safe.

Read guide →

Ship a real React Native app today

Describe, preview, and export Expo code — free to start.

Build with ShipNative →
ShipNative logoShipnative

Build mobile apps with AI. Describe, preview, and ship to iOS & Android in minutes.

Features

Text to App AIApp Generator from ScreenshotPRD to Mobile App

Tools

All free toolsApp Cost CalculatorApp Name GeneratorApp Store Keyword ToolReact Native Components

Blog

All blog postsHow to Build an App Without CodingBest AI Tools for Real Mobile AppsExpo EAS App Store ChecklistLovable, Cursor & v0 for MobileBest AI App Builders in 2026React Native AI App Builder Guide

Legal

FAQTerms of ServicePrivacy Policy

© 2026 ShipNative. All rights reserved.