webdevpuneet.com

Swipe-to-Reveal List Item Actions — Archive/Delete with Real Drag Physics

Swipe-to-Reveal List Item Actions (Archive / Delete) · Mobile · Plain HTML, CSS & JS · Live preview

Copy & paste

Source Code

<div class="demo">
  <div class="phone-frame">
    <div class="inbox-screen">
      <div class="inbox-header">Inbox</div>
      <ul class="swipe-list" id="swipeList">
        <li class="swipe-item" data-id="1">
          <div class="swipe-actions">
            <button class="swipe-action archive" data-action="archive">Archive</button>
            <button class="swipe-action delete" data-action="delete">Delete</button>
          </div>
          <div class="swipe-content">
            <span class="swipe-avatar">MC</span>
            <div class="swipe-text">
              <span class="swipe-title">Mia Chen</span>
              <span class="swipe-preview">Approved the design changes for the...</span>
            </div>
          </div>
        </li>
        <li class="swipe-item" data-id="2">
          <div class="swipe-actions">
            <button class="swipe-action archive" data-action="archive">Archive</button>
            <button class="swipe-action delete" data-action="delete">Delete</button>
          </div>
          <div class="swipe-content">
            <span class="swipe-avatar">SO</span>
            <div class="swipe-text">
              <span class="swipe-title">Sam Okoye</span>
              <span class="swipe-preview">Can we move Thursday's sync to...</span>
            </div>
          </div>
        </li>
        <li class="swipe-item" data-id="3">
          <div class="swipe-actions">
            <button class="swipe-action archive" data-action="archive">Archive</button>
            <button class="swipe-action delete" data-action="delete">Delete</button>
          </div>
          <div class="swipe-content">
            <span class="swipe-avatar">JP</span>
            <div class="swipe-text">
              <span class="swipe-title">Jules Park</span>
              <span class="swipe-preview">Here's the updated budget spreadsheet...</span>
            </div>
          </div>
        </li>
      </ul>
      <p class="swipe-hint">Drag a row left to reveal actions. Drag with a mouse too — the interaction works with both touch and pointer input.</p>
    </div>
  </div>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Row content follows the pointer's live position throughout the drag, not a preset fixed-distance animation
Drag position is clamped to a valid range between fully-open and fully-closed on every single movement, regardless of pointer travel distance
Genuine flick/threshold-based open-vs-closed decision on release, not an all-or-nothing single-tap trigger
Tapping an already-open row's content closes it, offering a second natural dismissal path beyond dragging back
Delete action sequences a content fade followed by a height collapse, rather than an abrupt instant removal
Archive action gives a brief visual confirmation flash before automatically closing the revealed actions
Built entirely on Pointer Events with setPointerCapture, working identically for touch and mouse input

About this UI Snippet

Swipe-to-Reveal List Actions — Getting the Drag Physics Right

Screenshot of the Swipe-to-Reveal List Item Actions (Archive / Delete) snippet rendered live

The swipe-left-to-reveal-actions pattern is familiar from every mobile mail and messaging app — but a convincing implementation depends on getting the *drag physics* right, not just sliding a panel a fixed distance on tap. This snippet tracks the actual pointer position throughout the gesture, clamps it to a valid range, and decides open-vs-closed based on how far the drag traveled relative to a real threshold — the same feel as a native swipe action.

The row's content follows the pointer exactly, not a preset animation

While dragging, setTranslate() is called on every pointermove with a position computed directly from the pointer's live delta — the row's content visually tracks the finger or cursor in real time, rather than snapping through a fixed pre-built animation the instant a swipe is detected. This is what makes the interaction feel physically connected to the gesture rather than merely triggered by it.

Clamping keeps the drag within a valid range, always

Math.min(0, Math.max(-ACTIONS_WIDTH, currentX + delta)) bounds every intermediate drag position between fully-closed (0) and fully-open (-ACTIONS_WIDTH, exactly the combined width of the two action buttons) — no matter how far past either edge the pointer actually travels. Without this clamp, a fast or long drag could push the content translateX far beyond the actions' actual width, either revealing empty space beyond the buttons or over-dragging in the closed direction into negative content overlap.

A genuine flick threshold, not "any drag opens it"

endDrag() decides whether to snap open or closed based on whether the final drag position passed the halfway point of ACTIONS_WIDTH — not whether *any* drag distance was registered at all. This matches how real native swipe interactions feel: a small, tentative drag that doesn't cross the threshold snaps back closed, while a more committed drag past the midpoint snaps fully open, giving the user forgiving, natural-feeling control rather than an all-or-nothing trigger.

Tapping open content closes it — a second, independent way back

Beyond dragging back to closed, a plain click on an already-open row's content also closes it. This matters because not every user will think to drag back; tapping the now-familiar visible content area to dismiss revealed actions is a common alternate expectation, and implementing it as a separate, simple click handler (checking isOpen first) covers that case without complicating the drag logic itself.

Delete animates height *after* the content fade completes, not simultaneously

The .removing class first fades the row's content opacity, and — via a CSS transition-delay on the outer <li>'s max-height — only *then* collapses the row's height to zero. Collapsing height and fading content at exactly the same time would look like the text is being physically squashed as it disappears; sequencing fade-then-collapse instead produces a cleaner, more intentional-feeling removal.

Step by step

How to Use

  1. 1
    Drag a row left with a mouse or touchThe row's content visually tracks the drag in real time, revealing the archive/delete actions underneath it as it slides.
  2. 2
    Release partway through a short dragIf you didn't drag past the halfway point of the actions' width, the row snaps back closed automatically.
  3. 3
    Release after dragging past the halfway pointThe row snaps fully open, staying revealed until you interact with it again.
  4. 4
    Tap the open row's content, or drag it back rightBoth close the row — tapping is a quick way to dismiss without needing to reverse the drag gesture.
  5. 5
    Tap "Delete"The row's content fades first, then its height smoothly collapses to zero before it's removed from the DOM.

Real-world uses

Common Use Cases

INBOX
Mail and messaging inbox lists
The canonical mobile pattern — swipe an email or message to archive or delete without opening it first.
TODO
Task and to-do list items
Swipe-to-complete or swipe-to-delete task rows in a mobile productivity app.
NOTIFICATION
Notification center list items
Dismissible notification rows benefit from the same swipe-reveal pattern for quick, one-handed dismissal.
CART
Mobile cart or list item removal
Shopping cart or saved-items lists where swiping to reveal a remove action keeps the primary tap target focused on the item itself.
Related: Sticky Footer CTA Bar with Safe-Area Insets
See the Sticky Footer CTA Bar with Safe-Area Insets for a related mobile pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Tracking the pointer's live position on every move makes the interaction feel physically connected to the gesture — the same feel users expect from native swipe actions on their phone, rather than a UI element that merely reacts to a swipe being detected after the fact.

Whether the final drag position passed the halfway point between fully-closed and fully-open (ACTIONS_WIDTH / 2) — a real threshold-based decision, not simply whether any dragging happened at all. This gives forgiving, natural-feeling control matching real native swipe interactions.

Yes — tapping anywhere on the open row's visible content closes it via a separate click handler, giving users a second, simpler way to dismiss the revealed actions besides reversing the drag gesture.

Fading the content first and only then collapsing the row's height (via a CSS transition-delay) avoids the visually awkward effect of text appearing to be physically squashed as the row shrinks — sequencing the two produces a cleaner, more intentional-feeling removal animation.

The drag position is clamped with Math.min/Math.max on every movement to stay within the valid [-ACTIONS_WIDTH, 0] range, so no amount of extra drag distance can push the row translation beyond fully-open or fully-closed — it simply stops moving once it hits either boundary.

Both — the interaction is built entirely on Pointer Events (pointerdown/pointermove/pointerup/pointercancel) with setPointerCapture, which unifies mouse, touch, and pen input under one API, so dragging with a mouse produces identical behavior to a finger swipe.