1 slot open this monthBy the makers of shadcnregistryWe built shadcnregistry. Now we build your product.Book a callCheck availability
1 slot open this monthAI features?We build AI features into your product.Book a callCheck availability
1 slot open this month🚨 Slop police🚨 Slop police: we clean up vibe-coded apps.Report itReport your app
shadcnregistry
shadcnregistry
RegistryHealthInspect
Terms of ServicePrivacy Policy
  1. Registries
  2. pulld
  3. Swipe Actions

Swipe Actions

ui

A list row that slides sideways under a thumb to reveal what can be done to it — archive, delete, mark read, pin, snooze — with a button on the row that reveals exactly the same actions, because a swipe is not a control a keyboard or a screen reader has. Reach for it wherever a list is worked through rather than read: a mail or message inbox, a chat or conversation list, notifications and an activity feed, a task or to-do list, cart and wishlist rows, saved or favourite items, an orders or deliveries screen, a support or ticket queue, a moderation queue, and any mobile admin list where a row needs an action but not a whole screen of its own. Common asks it answers: "swipe actions react", "swipe to delete react", "swipe to reveal actions", "swipeable list item react", "swipe left to delete list row", "ios mail swipe actions web", "react-swipeable-list alternative", "react-swipe-to-delete-component alternative", "swipeable-list-item shadcn", "shadcn swipe actions", "shadcn swipe to delete", "swipe actions without a library", "swipe gesture react no dependencies", "framer-motion drag row actions", "touch swipe row react", "detect horizontal swipe vs vertical scroll", "swipe breaks vertical scrolling", "list wont scroll after adding swipe", "Unable to preventDefault inside passive event listener invocation", "onTouchMove preventDefault not working in react", "touch-action pan-y", "swipe actions rtl", "swipe direction reversed in arabic", "accessible swipe actions", "swipe to delete keyboard accessible", "screen reader cannot swipe", "row click fires after swipe", "click fires when i swipe a list item", "snap open swipe row", "swipe row flick velocity", "close other rows when one is swiped", "スワイプ 削除 react", "リストを横に払って操作". Official shadcn/ui has nothing here and nothing to build it out of. Fetching every entry in its registry today — 63 listed, 62 fetchable, questionnaire is indexed and 404s on both style tracks — and grepping all 211,787 bytes of source, touchstart, touchmove, touchend, touchcancel, onTouchStart, onTouchMove, pointerdown, pointermove, pointerup, setPointerCapture, releasePointerCapture, onSwipe, threshold, TouchEvent, PointerEvent, clientX, deltaX and passive are every one of them a zero hit (measured 2026-09-30): it does not read a drag anywhere. The seven hits for the word swipe are all one Tailwind class string on toast, the Radix data-[swipe=…] variants that flick a transient notification away, and the single translateX is the progress bar's own fill. Distinct from the pulld pieces it sits beside, which each take a different half of the problem: sortable-list drags a row to move it in the order rather than to act on it, pull-to-refresh reads a vertical pull at the top of the same list, bulk-action-bar acts on rows after they have been ticked rather than one at a time, toast has a swipe of its own but it dismisses a notification outright and offers nothing to choose from, inline-edit changes a row's contents in place, and confirm-button and type-to-confirm guard the destructive action this reveals. Seven things separate it from the version built out of an onTouchMove and a clientX subtraction. First, the axis is decided once, after the finger has left an 8px slop circle, and a tie goes to vertical — the opposite of the same tie-break in a pull-to-refresh, and deliberately: this component is the row inside the list, so what it can wreck is the list's scrolling, and a list that stops moving under a thumb reads as broken while a swipe that needs a second try reads as a miss. Thumbs travel in arcs, so a decided axis is never re-decided. Second, touchmove is registered by hand with passive: false, because React registers onTouchMove passively and a passive listener's preventDefault is dropped with a console warning — the row slides sideways and the list scrolls behind it, and no prop changes that. touch-action: pan-y hands vertical scrolling back to the browser while keeping the horizontal direction, which is also what gives the preventDefault something to prevent: with the default auto the browser would have started scrolling and marked the sequence non-cancelable before the axis was decided. Third, the opening direction is derived from the resolved writing direction rather than from a hard-coded minus sign: the panel is positioned with inset-inline-end so the browser places it, and on an RTL page it sits on the left and the finger goes right to open it — a component that only knows how to subtract opens the row when it is swiped shut. That direction is read at event time, never during render, because getComputedStyle does not exist on the server and a transform decided in render would be missing from the server's HTML, which React discards the whole tree over. Fourth, the gestures that are not swipes are each turned away and each leaves the row where it already was, not shut: a second finger, a vertical drag, and a move the browser has already committed to scrolling. Returning an open row to shut is the bug where the row someone just opened vanishes as they scroll to reach its buttons. A drag that begins on an already-open row continues from where it was rather than jumping to zero under the finger. Fifth, a release snaps, and speed is read before distance: a short fast flick opens a row that never travelled half the panel, which is the gesture people actually make and the one a distance-only test refuses, and a fast flick back closes one that is still more than half open. A touchcancel — an incoming call, the system's own edge gesture — puts the row back instead of sharing the release handler and revealing actions nobody released onto. Sixth, the click a touch synthesises when it ends is swallowed in the capture phase after a swipe, so the row's own handler does not open the message that was being swiped. Seventh, the actions are reachable without the gesture: a disclosure button is always on the row carrying aria-expanded and aria-controls, the panel is a labelled group, and while it is shut its buttons are out of the tab order and out of the accessible tree — completely covered by the row, a focusable button is a focus ring vanishing into a list, which is worse than no keyboard support at all. Escape closes and hands focus back to that button, and so does running an action, so nobody loses their place. Deliberately reveal-only: a long swipe does not fire the destructive action by itself, since an accidental delete is the one mistake this shape of UI makes, and an auto-commit only touch users can reach is the same inequity the component exists to fix. The drag also stops exactly where the last action ends rather than rubber-banding past it, because there is nothing further back there to see. Controlled or not: pass actions and children and it manages itself, or add open and onOpenChange to hold "which row is open" in the list's own state and get one-at-a-time behaviour. Optional side (start/end), disabled, hideToggle for callers supplying their own control, actionWidth, openRatio, rowClassName and labels to reword or translate. One file of 747 lines behind 34 unit tests, shadcn tokens throughout so it follows light and dark themes and honours prefers-reduced-motion, and lucide-react is the only dependency — no gesture library, no Radix, no react-dom import.

pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/swipe-actions.json"
Premium blocks for shadcn/uiShadcnblocks.com · AffiliateExplore
  • lucide-react
    components/ui/swipe-actions.tsx