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

swipe-row

v1.1.0

Published

Swipeable rows for React on the web: reveal buttons like iOS Mail, or full-swipe to delete, archive or mark as read like Telegram and Gmail, with a threshold you can feel and an undo bar. Pointer Events, zero dependencies.

Readme

swipe-row

npm bundle size license types Live Demo

Swipeable rows for React on the web. Reveal buttons like iOS Mail, full-swipe to delete, archive or mark as read like Telegram and Gmail, or both in one row. Comes with an undo bar for the swipe you didn't mean. Zero dependencies.

👀 Try it: swipe-row.onrender.com. It works with a mouse too.

Features

  • Two patterns, or both at once. actions reveal buttons and the row stays open (iOS Mail). left / right are full-swipe actions: swipe past the line and the action happens, let go short and the row springs back (Telegram, Gmail). Put both on one side and a short swipe opens the buttons while a long one commits.
  • The line is something you feel. Past it, the icon jumps and onArmedChange fires. With haptics, the phone vibrates where the browser allows it (Android Chrome).
  • 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. An action can remove the row (after: 'remove', e.g. delete or archive) or 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.
  • It behaves well on a page.
    • Vertical scrolling keeps working.
    • The click a drag ends in never reaches your row's content.
    • A tap on an open row closes it.
    • Modals stay above the list.
  • It's accessible.
    • Buttons open the row when they get keyboard focus.
    • Each full-swipe action gets a hidden button for screen readers and the Tab key. Focusing it lets the action peek out, and Enter commits it.
    • The undo bar is a live region.
  • Zero dependencies, about 5 kB gzipped. Built on Pointer Events and a small spring, and it honours prefers-reduced-motion.

Installation

npm install swipe-row

The only peer dependency is React (react and react-dom, 18 or newer).

Upgrading from 1.0? framer-motion is no longer needed. You can remove it if nothing else in your app uses it. The 1.0 props work as before, with one change in behaviour: a row now closes after one of its buttons is clicked. Pass closeOnAction={false} to keep it open.

Usage

Import the component and its CSS:

import { SwipeRow, TrashIcon } from 'swipe-row';
import 'swipe-row/style.css';

Full swipe

<SwipeRow
  left={{ label: 'Delete', icon: <TrashIcon />, onAction: () => remove(mail.id) }}
  right={{ label: 'Read', icon: <CheckIcon />, after: 'reset', onAction: () => markRead(mail.id) }}
  haptics
>
  <MailRow mail={mail} />
</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.

| 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's off screen | delete, archive, move | | 'reset' | springs back | at once | mark as read, pin, star |

Buttons (iOS Mail)

<SwipeRow
  actions={[
    { label: 'Reply', onClick: reply, backgroundColor: '#3b82f6' },
    { label: 'Archive', onClick: archive, backgroundColor: '#f59e0b', width: 90 },
  ]}
  leadingActions={[{ label: 'Pin', onClick: pin, backgroundColor: '#10b981' }]}
>
  <MailRow mail={mail} />
</SwipeRow>

actions show up on the right when the row is swiped left, and leadingActions on the left when it's swiped right. The row opens by the buttons' total width and closes after a button is clicked (closeOnAction).

Both on one side

<SwipeRow
  actions={[
    { label: 'Flag', onClick: flag, backgroundColor: '#f59e0b' },
    { label: 'Delete', onClick: remove, backgroundColor: '#ef4444' },
  ]}
  left={{ label: 'Delete', icon: <TrashIcon />, onAction: remove }}
>

A short swipe opens Flag and Delete. A long one, past the buttons and past threshold, deletes straight away.

Undo

import { SwipeRow, TrashIcon, UndoBar, useUndo } from 'swipe-row';

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

{mails.map((mail) => (
  <SwipeRow
    key={mail.id}
    left={{
      label: 'Delete',
      icon: <TrashIcon />,
      onAction: () => {
        undo.push(mail);
        setMails((list) => list.filter((m) => m.id !== mail.id));
      },
    }}
  >
    <MailRow mail={mail} />
  </SwipeRow>
))}

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

Confirm first

<SwipeRow
  left={{
    label: 'Delete',
    icon: <TrashIcon />,
    onAction: () => confirmDialog('Delete this message?'), // resolves true or false
  }}
>

If the promise resolves to false or rejects, the row comes back. The same works for a network call.

From your own code

const row = useRef<SwipeRowHandle>(null);

<SwipeRow ref={row} left={{ label: 'Delete', onAction: remove }}>…</SwipeRow>

row.current?.trigger('left'); // commit, with the same animation
row.current?.open('left');    // open on the buttons
row.current?.close();

For "only one row open at a time", keep the open row's id from onOpenChange and call close() on the others.

Props

SwipeRow

| Prop | Type | Default | Description | |---|---|---|---| | children | ReactNode | Required | The visible content of the row. | | actions | SwipeAction[] | | Buttons on the right, revealed by swiping left. | | leadingActions | SwipeAction[] | | Buttons on the left, revealed by swiping right. | | left | FullSwipeAction | | Full-swipe action for swiping left. | | right | FullSwipeAction | | Full-swipe action for swiping right. | | maxSwipe | number | sum of widths | Override how far actions open (negative px). | | threshold | number | 0.4 | Share of the width past which letting go commits. With buttons on that side, never closer than just past them. | | flickVelocity | number | 1000 | px/s for a flick to count. | | flickThreshold | number | 0.2 | Share of the width a flick must cover. | | sweepDuration | number | 180 | ms the row takes to leave. | | spring | { stiffness, damping, mass } | { 380, 38, 1 } | The settle. | | onArmedChange | (armed, side) => void | | The finger crossed the line, either way. | | haptics | boolean | false | Vibrate on crossing, where supported. | | onOpenChange | (side \| null) => void | | The row opened on its buttons, or closed. | | closeOnAction | boolean | true | Close after a button is clicked. | | disabled | boolean | false | | | className, style | | | Root container. | | innerClassName, innerStyle | | | The sliding foreground. |

SwipeAction (buttons)

| Property | Type | Default | Description | |---|---|---|---| | label | ReactNode | Required | Text or icon. | | onClick | () => void | Required | | | backgroundColor | string | '#64748b' | | | color | string | 'white' | | | width | number | 80 | px; the row opens by the total. | | className | string | | | | ariaLabel | string | label if a string | |

FullSwipeAction

| Property | Type | Default | Description | |---|---|---|---| | onAction | () => void \| boolean \| Promise<void \| boolean> | Required | false / rejected brings a removed row back. | | after | 'remove' \| 'reset' | 'remove' | See above. | | label | ReactNode | | Under the icon; names the hidden button. | | icon | ReactNode | | TrashIcon, ArchiveIcon, CheckIcon are included. | | color | string | red / blue | Red for 'remove', blue for 'reset'. | | textColor | string | 'white' | | | content | ReactNode | | Replaces icon and label. | | ariaLabel | string | label if a string | |

useUndo<T>({ timeout = 5000 }) and UndoBar

useUndo returns { item, push(item), take(), clear() }. Pushing again restarts the countdown, and take() returns the held item once.

<UndoBar visible message actionLabel="Undo" onAction className style /> fades in and out with visible.

Local development

  1. Clone the repository
  2. Install dependencies: npm install
  3. Start the playground: npm run dev
  4. Run unit tests: npm run test
  5. Build the library: npm run build

License

MIT © nahkar. Free to use in personal and commercial projects.