npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-full-swipe

v0.1.0

Published

Full-swipe actions for React Native rows: swipe past a threshold you can feel to delete, archive or mark as read, with a different action on each side and an undo bar. Reanimated + Gesture Handler, UI thread, TypeScript.

Readme

react-native-full-swipe

npm license types platforms

Full-swipe actions for React Native rows. Swipe a row past the line to delete it, archive it, mark it as read, pin it: a different action on each side. Comes with an undo bar for the swipe you didn't mean.

Most swipeable rows stop halfway and show a few buttons (the iOS Mail pattern). This package does the other pattern, the one Telegram and Gmail use: the row doesn't stop. The action's colour fills the whole gap the row opens, and when you let go, the action either happens or the row springs back.

  • The line is something you feel. Past it, the icon jumps and onArmedChange fires. That's where your haptic goes. Back off, and it jumps back.
  • A flick counts, a twitch doesn't. A fast flick commits from a shorter swipe, but only once the row is a fair way over.
  • Each side has its own action. One can remove the row (after: 'remove', e.g. delete or archive). The other can leave it in place (after: 'reset', e.g. mark as read or pin).
  • A removal can be confirmed, or can fail. Return false, a promise of it, or a rejected promise, and the row comes back.
  • Undo is included. useUndo + UndoBar hold the last removed item for a few seconds.
  • Screen readers get the actions too. Every side is also an accessibility action.
  • It runs on the UI thread. Built on Reanimated and Gesture Handler. No SVG dependency, no styling library.

Install

npm install react-native-full-swipe

Peer dependencies you most likely already have:

npx expo install react-native-reanimated react-native-gesture-handler

Wrap your app in GestureHandlerRootView if you haven't already.

Quick start

import { SwipeRow, TrashIcon } from 'react-native-full-swipe';

<SwipeRow
  left={{ label: 'Delete', icon: <TrashIcon />, onAction: () => remove(item.id) }}
  right={{ label: 'Read', icon: <CheckIcon />, after: 'reset', onAction: () => markRead(item.id) }}
  onArmedChange={(armed) => armed && Haptics.impactAsync()}
>
  <MessageCard item={item} />
</SwipeRow>

left is the action for swiping the row to the left, which uncovers the right-hand side. right is the action for swiping it to the right. Give only one of them and the row only moves that way.

The row's content has to be opaque, or the colour shows through it. Give borderRadius the same corner radius as your rows, so the corners that come out from under a row match it.

What happens after the swipe

| after | The row… | onAction runs… | Use for | |---|---|---|---| | 'remove' (default) | slides off the edge and stays there until you take it out of your list | once it is off screen | delete, archive, move to folder | | 'reset' | springs back | at once | mark as read, pin, star, snooze |

Undo

import { SwipeRow, TrashIcon, UndoBar, useUndo } from 'react-native-full-swipe';

const undo = useUndo<Drink>({ timeout: 5000 });

{drinks.map((drink) => (
  <SwipeRow
    key={drink.id}
    left={{
      label: 'Delete',
      icon: <TrashIcon />,
      onAction: () => {
        undo.push(drink);
        setDrinks((list) => list.filter((d) => d.id !== drink.id));
      },
    }}
  >
    <DrinkCard drink={drink} />
  </SwipeRow>
))}

<UndoBar
  visible={undo.item !== null}
  message={`${undo.item?.ml} ml removed`}
  onAction={() => {
    const back = undo.take();
    if (back) setDrinks((list) => [back, ...list]);
  }}
/>

Confirming a removal

<SwipeRow
  left={{
    label: 'Delete',
    icon: <TrashIcon />,
    onAction: () =>
      new Promise<boolean>((resolve) =>
        Alert.alert('Delete?', undefined, [
          { text: 'Keep', style: 'cancel', onPress: () => resolve(false) },
          { text: 'Delete', style: 'destructive', onPress: () => { remove(item); resolve(true); } },
        ]),
      ),
  }}
>

The same works for a network call: return its promise. If the promise rejects, the row comes back.

A row with other gestures

SwipeRow is two parts put together. Use them directly when the row also has its own gestures (a long press to reorder, say) or its own transforms:

import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, { useAnimatedStyle } from 'react-native-reanimated';
import { FullSwipeBackdrop, useFullSwipe } from 'react-native-full-swipe';

function Row({ onDelete, children }) {
  const swipe = useFullSwipe({ left: { label: 'Delete', onAction: onDelete } });
  const { translateX } = swipe; // take it out before using it in a worklet
  const style = useAnimatedStyle(() => ({ transform: [{ translateX: translateX.value }] }));

  return (
    <View onLayout={swipe.onLayout}>
      <FullSwipeBackdrop swipe={swipe} />
      <GestureDetector gesture={Gesture.Race(swipe.gesture, yourLongPress)}>
        <Animated.View style={style}>{children}</Animated.View>
      </GestureDetector>
    </View>
  );
}

Take shared values out of swipe before you use them in a worklet. A worklet that reaches through swipe would try to copy the gesture object onto the UI thread, and that fails.

Accessibility

A swipe is a gesture that not everyone can make, so every side is also a screen-reader action, named by its label. Pass a function as the child and spread what it gets onto the row's focusable element:

<SwipeRow left={{ label: 'Delete', onAction: remove }} right={{ label: 'Read', after: 'reset', onAction: read }}>
  {(a11y) => (
    <Pressable onPress={open} accessibilityLabel={`${item.from}: ${item.text}`} {...a11y}>
      <MessageCard item={item} />
    </Pressable>
  )}
</SwipeRow>

With the hook, spread swipe.accessibilityProps the same way. ref.trigger('left') (or swipe.trigger) commits an action from a button or a menu, with the same animation.

The backdrop is hidden from screen readers, because the action happens when you let go, not when you tap the backdrop. UndoBar is a polite live region, so the removal is announced without moving focus.

API

SwipeAction

| Field | | | |---|---|---| | onAction | required | () => void \| boolean \| Promise<void \| boolean> | | after | 'remove' | 'remove' or 'reset', see above. | | label | | Text under the icon. Also names the accessibility action. | | icon | | Any node. TrashIcon is included, or bring your own icon set. | | color | red / blue | Red for 'remove', blue for 'reset'. | | labelStyle | | | | content | | Replaces icon and label. It still grows and jumps with the swipe. | | accessibilityLabel | label | |

useFullSwipe(options)

| Option | Default | | |---|---|---| | left, right | | SwipeActions. | | enabled | true | | | threshold | 0.4 | Share of the row's width past which letting go commits. | | flickVelocity | 1200 | Points per second for a flick to count. | | flickThreshold | 0.2 | Share of the width the row must be over for a flick to count. | | sweepDuration | 180 | ms the row takes to leave. | | springConfig | { damping: 18, stiffness: 240, mass: 0.6 } | The spring back. | | activeOffsetX | 14 | Sideways travel before the swipe takes over. | | failOffsetY | 12 | Vertical travel before it gives up, so the list still scrolls. | | onArmedChange | | (armed, side) => void on the JS thread when the finger crosses the line. | | gestureTestId | | For Gesture Handler's getByGestureTestId in tests. |

Returns { gesture, translateX, armed, width, onLayout, trigger(side), reset(), accessibilityProps, left, right, threshold }.

<FullSwipeBackdrop swipe borderRadius style>

Draws each side's colour, icon and label behind the row.

<SwipeRow>

All hook options, plus borderRadius, style (outer view), rowStyle (sliding view), backdropStyle, testID, and a ref with trigger(side) / reset(). children can be a node or (accessibilityProps) => node.

useUndo<T>({ timeout = 5000 })

Returns { item, push(item), take(), clear() }. Pushing again restarts the countdown, even for an equal item. take() returns the held item once and stops holding it.

<UndoBar visible message actionLabel onAction accentColor style messageStyle actionStyle>

Fades in and out with visible. message can be a string or your own node.

Testing

Pass gestureTestId and drive the gesture with Gesture Handler's jest utils:

fireGestureHandler(getByGestureTestId('swipe'), [
  { state: State.BEGAN, translationX: 0 },
  { state: State.ACTIVE, translationX: -20 },   // the first ACTIVE event starts the gesture
  { state: State.ACTIVE, translationX: -200 },
  { state: State.END, translationX: -200 },
]);
jest.advanceTimersByTime(1000);                 // let the sweep finish

The row needs a width for the threshold to mean anything, so fire layout on it first.

License

MIT