Swipe to Delete List — Touch Gesture HTML CSS JS Snippet

Swipe to Delete List · Layouts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Unified touch + mouse drag handling — works on mobile and desktop
Threshold-based commit (80px) with snap-back below threshold
Red delete reveal background with opacity proportional to swipe distance
max-height collapse animation for smooth item removal
Unread dot system with live badge count updates
Click-to-delete X button alongside swipe gesture
Empty state when all items are dismissed
no-transition during drag prevents rubber-band lag

About this UI Snippet

Swipe-to-Delete List — Touch & Mouse Drag, Threshold Delete & Collapse Animation

Screenshot of the Swipe to Delete List snippet rendered live

Swipe-to-delete is the dominant gesture pattern for list management on mobile — popularised by iOS Mail and Android Gmail, it lets users dismiss items with a natural leftward swipe without needing a separate edit mode or confirmation dialog. This snippet implements a full swipe-to-delete notification list that works on both touch (mobile) and mouse (desktop drag), with a threshold-based delete trigger, smooth height collapse animation, and an unread count badge.

Pointer event architecture: unified touch + mouse

The gesture handler listens for both touchstart/touchmove/touchend and mousedown/mousemove/mouseup. This single-handler approach avoids PointerEvents API (which has quirks with passive listeners on iOS) while covering all input types. The key trick: mousemove and mouseup listeners are attached to document on drag start (not on the element), so the drag continues even if the cursor leaves the list item during fast movement.

Threshold detection and snap behaviour

The reveal distance is tracked in currentX (clamped to Math.min(0, x) — left-only swipe). When pointerUp fires, if Math.abs(currentX) >= THRESHOLD (80px), the item is committed to deletion: the content is animated to translateX(-100%) then deleteItem() fires after 250ms. If below threshold, the item snaps back to translateX(0). The delete background opacity is driven by progress: Math.min(1, Math.abs(currentX) / THRESHOLD), so it fades in proportionally to swipe distance.

Height collapse animation

When an item is deleted, the .removing class is added which sets max-height: 0 and opacity: 0 (with CSS transitions). After 380ms (matching the transition duration), li.remove() is called to clean up the DOM. This max-height collapse technique is a CSS-only approach to height transitions — animating height from a computed value is impossible, but animating from a known max down to 0 works in all browsers.

Unread dot and badge

Items with unread notifications have a .item-dot.unread element (a small blue circle). After each deletion, updateBadge() counts remaining .unread dots and updates the badge number. If count reaches 0, the badge is hidden. This is a realistic implementation pattern for notification UIs.

`no-transition` class during drag

During active drag, the .no-transition class is added to the swipe track, which sets transition: none on .item-content. Without this, every pixel of drag movement would trigger a CSS transition, causing a laggy "rubber band" effect. The transition is only re-enabled on pointer release, so the snap-back and delete animations run smoothly.

React integration

In React, maintain items array in state. Each item has an id and removing boolean. Swipe handlers update a swipeOffsets ref (not state, to avoid re-renders during drag). On pointerUp, check threshold and call setItems(prev => prev.filter(i => i.id !== id)). Use useRef for the drag state. Apply className={removing ? 'removing' : ''} on the list item. Use a useEffect cleanup to remove document event listeners on unmount.

See also the checkout form snippet for form step UX patterns, the progress wizard snippet for multi-step flows, and the floating chat widget snippet for notification-style overlays.

Step by step

How to Use

  1. 1
    Copy all three blocksPaste the HTML, CSS, and JS. Five notification items render in a card panel with gradient avatars and timestamps.
  2. 2
    Try swipe on mobileOn a touch device, swipe any item left. A red delete background reveals beneath. Swipe past 80px to trigger deletion, or release early to snap back.
  3. 3
    Try drag on desktopClick and drag an item to the left with your mouse. The same threshold logic applies — drag past 80px to delete.
  4. 4
    Use the X buttonEach item has a small X button on the right for click-to-delete without swiping — useful for mouse-first users.
  5. 5
    Watch the badge updateThe red notification badge counts unread items (those with blue dots). It decrements as unread items are deleted.
  6. 6
    Dismiss all itemsDelete all five items to reveal the empty state — a grey icon and "All caught up!" message.

Real-world uses

Common Use Cases

Notification Panels
Swipeable notification list in mobile apps and web dashboards
Task and To-do Lists
Swipe-to-complete or swipe-to-dismiss gesture for task management UIs
Email and Inbox UIs
Gmail-style swipe to archive or delete email list items
Chat Message Lists
Swipe to delete messages in a chat UI — common pattern in messaging apps

Got questions?

Frequently Asked Questions

Extend the handler to track positive currentX values. Add an archive-bg div on the left side. When swipe right exceeds threshold, trigger archive instead of delete. Use green for archive, red for delete.

On delete, instead of immediately removing the item, mark it as removed in state but keep it in the DOM for 5 seconds. Show a "Undo" snackbar. If undo clicked, un-mark it. After 5s with no undo, call the actual delete API.

Use useRef for drag state (not useState — avoid re-renders during drag). Attach event listeners via useEffect. Store item removal in useState with a removing flag for the CSS animation, then filter the item from state after the animation duration.

Call e.preventDefault() inside touchmove only when horizontal movement exceeds ~5px (to distinguish horizontal swipe from vertical scroll). Check Math.abs(deltaX) > Math.abs(deltaY) before starting the swipe gesture.

Open the Export menu (or the Test Exports preview) in the snippet toolbar. It generates a plain React component, a React + Tailwind version where the row and delete-background styles become utility classes, a Vue 3 single-file component, and an Angular standalone component. Each converter preserves the markup, the collapse animation, and the touch/mouse drag behaviour, so the gesture works identically across React, Vue, and Angular. In React, keep the live drag offset in a useRef so dragging does not trigger re-renders, attach the pointer listeners in useEffect, and only move item removal into useState when you run the collapse animation — the same pattern maps to onMounted in Vue and ngAfterViewInit in Angular.

Swipe gestures are not keyboard accessible on their own, so reveal a real delete button on each row when it receives focus and wire it to the same delete function. Give each row role="listitem" inside a role="list" container, label the delete control with aria-label, and announce removals through an aria-live="polite" region so screen-reader users hear "Notification deleted" when an item disappears.

Swipe to Delete List — Export as HTML, React, Vue, Angular & Tailwind

HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Swipe to Delete List</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: system-ui, sans-serif; background: #f1f5f9; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 20px; }
    .demo { width: 100%; max-width: 360px; }
    .panel { background: #fff; border-radius: 20px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); overflow: hidden; }
    .panel-header { display: flex; align-items: center; gap: 8px; padding: 16px 18px 12px; border-bottom: 1px solid #f3f4f6; }
    .panel-title { font-size: 15px; font-weight: 800; color: #111827; }
    .badge { background: #ef4444; color: #fff; font-size: 10px; font-weight: 800; padding: 2px 7px; border-radius: 20px; }
    
    /* List */
    .list { list-style: none; }
    .list-item { position: relative; border-bottom: 1px solid #f3f4f6; overflow: hidden; transition: max-height 0.35s ease, opacity 0.25s ease; max-height: 80px; }
    .list-item:last-child { border-bottom: none; }
    .list-item.removing { max-height: 0; opacity: 0; }
    
    /* Swipe track */
    .swipe-track { position: relative; display: flex; align-items: stretch; cursor: grab; user-select: none; touch-action: pan-y; }
    .swipe-track.dragging { cursor: grabbing; }
    .delete-bg { position: absolute; right: 0; top: 0; bottom: 0; background: #ef4444; display: flex; align-items: center; gap: 6px; padding: 0 16px; color: #fff; font-size: 11px; font-weight: 700; min-width: 80px; justify-content: center; }
    .item-content { position: relative; display: flex; align-items: center; gap: 11px; padding: 12px 36px 12px 16px; background: #fff; width: 100%; transition: transform 0.15s ease; z-index: 1; }
    .swipe-track.no-transition .item-content { transition: none; }
    
    /* Item parts */
    .item-avatar { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 800; color: #fff; flex-shrink: 0; }
    .item-body { flex: 1; min-width: 0; }
    .item-title { font-size: 12px; font-weight: 600; color: #111827; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .item-sub { font-size: 10px; color: #9ca3af; margin-top: 2px; }
    .item-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
    .item-dot.unread { background: #3b82f6; }
    
    /* Delete button (always visible on right) */
    .del-btn { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); width: 22px; height: 22px; border-radius: 50%; border: none; background: #f3f4f6; color: #9ca3af; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 2; transition: background 0.12s, color 0.12s; }
    .del-btn:hover { background: #fee2e2; color: #ef4444; }
    
    /* Empty state */
    .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 40px 20px; }
    .empty-state p { font-size: 13px; color: #9ca3af; font-weight: 600; }
  </style>
</head>
<body>
  <div class="demo">
    <div class="panel">
      <div class="panel-header">
        <h2 class="panel-title">Notifications</h2>
        <span class="badge" id="badge">5</span>
      </div>
  
      <ul class="list" id="list">
        <li class="list-item" id="item-0">
          <div class="swipe-track" id="track-0">
            <div class="delete-bg">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content">
              <div class="item-avatar" style="background:linear-gradient(135deg,#6366f1,#8b5cf6)">JD</div>
              <div class="item-body">
                <div class="item-title">Jane Doe liked your post</div>
                <div class="item-sub">2 minutes ago</div>
              </div>
              <div class="item-dot unread"></div>
            </div>
          </div>
          <button class="del-btn" onclick="deleteItem('item-0')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
  
        <li class="list-item" id="item-1">
          <div class="swipe-track" id="track-1">
            <div class="delete-bg">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content">
              <div class="item-avatar" style="background:linear-gradient(135deg,#0ea5e9,#06b6d4)">AK</div>
              <div class="item-body">
                <div class="item-title">Alex Kim commented on your PR</div>
                <div class="item-sub">15 minutes ago</div>
              </div>
              <div class="item-dot unread"></div>
            </div>
          </div>
          <button class="del-btn" onclick="deleteItem('item-1')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
  
        <li class="list-item" id="item-2">
          <div class="swipe-track" id="track-2">
            <div class="delete-bg">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content">
              <div class="item-avatar" style="background:linear-gradient(135deg,#f59e0b,#ef4444)">MP</div>
              <div class="item-body">
                <div class="item-title">Maria Perez shared a file with you</div>
                <div class="item-sub">1 hour ago</div>
              </div>
            </div>
          </div>
          <button class="del-btn" onclick="deleteItem('item-2')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
  
        <li class="list-item" id="item-3">
          <div class="swipe-track" id="track-3">
            <div class="delete-bg">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content">
              <div class="item-avatar" style="background:linear-gradient(135deg,#10b981,#059669)">TN</div>
              <div class="item-body">
                <div class="item-title">Tom Nelson merged your pull request</div>
                <div class="item-sub">3 hours ago</div>
              </div>
              <div class="item-dot unread"></div>
            </div>
          </div>
          <button class="del-btn" onclick="deleteItem('item-3')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
  
        <li class="list-item" id="item-4">
          <div class="swipe-track" id="track-4">
            <div class="delete-bg">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content">
              <div class="item-avatar" style="background:linear-gradient(135deg,#ec4899,#f43f5e)">SW</div>
              <div class="item-body">
                <div class="item-title">Sara White invited you to a project</div>
                <div class="item-sub">Yesterday</div>
              </div>
            </div>
          </div>
          <button class="del-btn" onclick="deleteItem('item-4')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
      </ul>
  
      <div class="empty-state" id="emptyState" style="display:none">
        <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#d1d5db" stroke-width="1.5" stroke-linecap="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
        <p>All caught up!</p>
      </div>
    </div>
  </div>
  <script>
    var startX = 0, currentX = 0, activeTrack = null, THRESHOLD = 80;
    
    function pointerDown(e, track) {
      activeTrack = track;
      startX = e.touches ? e.touches[0].clientX : e.clientX;
      currentX = 0;
      track.classList.add('no-transition', 'dragging');
      document.addEventListener('mousemove', pointerMove);
      document.addEventListener('mouseup', pointerUp);
      document.addEventListener('touchmove', pointerMove, { passive: false });
      document.addEventListener('touchend', pointerUp);
    }
    function pointerMove(e) {
      if (!activeTrack) return;
      var x = (e.touches ? e.touches[0].clientX : e.clientX) - startX;
      currentX = Math.min(0, x);
      var content = activeTrack.querySelector('.item-content');
      var reveal = Math.min(Math.abs(currentX), THRESHOLD + 20);
      content.style.transform = 'translateX(' + currentX + 'px)';
      var bg = activeTrack.querySelector('.delete-bg');
      bg.style.opacity = Math.min(1, Math.abs(currentX) / THRESHOLD);
      if (e.cancelable) e.preventDefault();
    }
    function pointerUp() {
      if (!activeTrack) return;
      activeTrack.classList.remove('no-transition', 'dragging');
      var content = activeTrack.querySelector('.item-content');
      if (Math.abs(currentX) >= THRESHOLD) {
        var li = activeTrack.closest('.list-item');
        content.style.transform = 'translateX(-100%)';
        setTimeout(() => deleteItem(li.id), 250);
      } else {
        content.style.transform = '';
        var bg = activeTrack.querySelector('.delete-bg');
        bg.style.opacity = '';
      }
      activeTrack = null;
      document.removeEventListener('mousemove', pointerMove);
      document.removeEventListener('mouseup', pointerUp);
      document.removeEventListener('touchmove', pointerMove);
      document.removeEventListener('touchend', pointerUp);
    }
    
    // Attach swipe handlers
    document.querySelectorAll('.swipe-track').forEach(function(track) {
      track.addEventListener('mousedown', function(e) { pointerDown(e, track); });
      track.addEventListener('touchstart', function(e) { pointerDown(e, track); }, { passive: true });
    });
    
    function deleteItem(id) {
      var li = document.getElementById(id);
      if (!li) return;
      li.classList.add('removing');
      setTimeout(function() {
        li.remove();
        updateBadge();
        checkEmpty();
      }, 380);
    }
    function updateBadge() {
      var count = document.querySelectorAll('.item-dot.unread').length;
      var badge = document.getElementById('badge');
      badge.textContent = count;
      badge.style.display = count > 0 ? '' : 'none';
    }
    function checkEmpty() {
      var items = document.querySelectorAll('.list-item');
      if (items.length === 0) {
        document.getElementById('emptyState').style.display = 'flex';
      }
    }
  </script>
</body>
</html>
Tailwind
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Swipe to Delete List</title>
  <!-- Tailwind CSS v3+ — arbitrary value classes (e.g. bg-[#0f172a]) are valid -->
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    * {
      box-sizing: border-box; margin: 0; padding: 0;
    }

    body {
      font-family: system-ui, sans-serif; background: #f1f5f9; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 20px;
    }

    .badge {
      background: #ef4444; color: #fff; font-size: 10px; font-weight: 800; padding: 2px 7px; border-radius: 20px;
    }

    .list-item:last-child {
      border-bottom: none;
    }

    .list-item.removing {
      max-height: 0; opacity: 0;
    }

    .swipe-track.dragging {
      cursor: grabbing;
    }

    .swipe-track.no-transition .item-content {
      transition: none;
    }

    .item-dot.unread {
      background: #3b82f6;
    }

    .empty-state p {
      font-size: 13px; color: #9ca3af; font-weight: 600;
    }
  </style>
</head>
<body>
  <div class="w-full max-w-[360px]">
    <div class="bg-[#fff] rounded-[20px] shadow-[0_8px_32px_rgba(0,0,0,0.1)] overflow-hidden">
      <div class="flex items-center gap-2 pt-4 px-[18px] pb-3 border-b border-b-[#f3f4f6]">
        <h2 class="text-[15px] font-extrabold text-[#111827]">Notifications</h2>
        <span class="badge" id="badge">5</span>
      </div>
  
      <ul class="[list-style:none]" id="list">
        <li class="list-item relative border-b border-b-[#f3f4f6] overflow-hidden [transition:max-height_0.35s_ease,_opacity_0.25s_ease] max-h-20" id="item-0">
          <div class="swipe-track relative flex items-stretch cursor-grab select-none [touch-action:pan-y]" id="track-0">
            <div class="delete-bg absolute right-0 top-0 bottom-0 bg-[#ef4444] flex items-center gap-1.5 py-0 px-4 text-[#fff] text-[11px] font-bold min-w-[80px] justify-center">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content relative flex items-center gap-[11px] pt-3 pr-9 pb-3 pl-4 bg-[#fff] w-full [transition:transform_0.15s_ease] z-[1]">
              <div class="w-9 h-9 rounded-[10px] flex items-center justify-center text-[11px] font-extrabold text-[#fff] shrink-0" style="background:linear-gradient(135deg,#6366f1,#8b5cf6)">JD</div>
              <div class="flex-1 min-w-0">
                <div class="text-xs font-semibold text-[#111827] leading-[1.4] whitespace-nowrap overflow-hidden text-ellipsis">Jane Doe liked your post</div>
                <div class="text-xs text-[#9ca3af] mt-0.5">2 minutes ago</div>
              </div>
              <div class="item-dot w-[7px] h-[7px] rounded-full shrink-0 unread"></div>
            </div>
          </div>
          <button class="absolute right-2 top-1/2 [transform:translateY(-50%)] w-[22px] h-[22px] rounded-full border-0 bg-[#f3f4f6] text-[#9ca3af] cursor-pointer flex items-center justify-center z-[2] [transition:background_0.12s,_color_0.12s] hover:bg-[#fee2e2] hover:text-[#ef4444]" onclick="deleteItem('item-0')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
  
        <li class="list-item relative border-b border-b-[#f3f4f6] overflow-hidden [transition:max-height_0.35s_ease,_opacity_0.25s_ease] max-h-20" id="item-1">
          <div class="swipe-track relative flex items-stretch cursor-grab select-none [touch-action:pan-y]" id="track-1">
            <div class="delete-bg absolute right-0 top-0 bottom-0 bg-[#ef4444] flex items-center gap-1.5 py-0 px-4 text-[#fff] text-[11px] font-bold min-w-[80px] justify-center">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content relative flex items-center gap-[11px] pt-3 pr-9 pb-3 pl-4 bg-[#fff] w-full [transition:transform_0.15s_ease] z-[1]">
              <div class="w-9 h-9 rounded-[10px] flex items-center justify-center text-[11px] font-extrabold text-[#fff] shrink-0" style="background:linear-gradient(135deg,#0ea5e9,#06b6d4)">AK</div>
              <div class="flex-1 min-w-0">
                <div class="text-xs font-semibold text-[#111827] leading-[1.4] whitespace-nowrap overflow-hidden text-ellipsis">Alex Kim commented on your PR</div>
                <div class="text-xs text-[#9ca3af] mt-0.5">15 minutes ago</div>
              </div>
              <div class="item-dot w-[7px] h-[7px] rounded-full shrink-0 unread"></div>
            </div>
          </div>
          <button class="absolute right-2 top-1/2 [transform:translateY(-50%)] w-[22px] h-[22px] rounded-full border-0 bg-[#f3f4f6] text-[#9ca3af] cursor-pointer flex items-center justify-center z-[2] [transition:background_0.12s,_color_0.12s] hover:bg-[#fee2e2] hover:text-[#ef4444]" onclick="deleteItem('item-1')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
  
        <li class="list-item relative border-b border-b-[#f3f4f6] overflow-hidden [transition:max-height_0.35s_ease,_opacity_0.25s_ease] max-h-20" id="item-2">
          <div class="swipe-track relative flex items-stretch cursor-grab select-none [touch-action:pan-y]" id="track-2">
            <div class="delete-bg absolute right-0 top-0 bottom-0 bg-[#ef4444] flex items-center gap-1.5 py-0 px-4 text-[#fff] text-[11px] font-bold min-w-[80px] justify-center">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content relative flex items-center gap-[11px] pt-3 pr-9 pb-3 pl-4 bg-[#fff] w-full [transition:transform_0.15s_ease] z-[1]">
              <div class="w-9 h-9 rounded-[10px] flex items-center justify-center text-[11px] font-extrabold text-[#fff] shrink-0" style="background:linear-gradient(135deg,#f59e0b,#ef4444)">MP</div>
              <div class="flex-1 min-w-0">
                <div class="text-xs font-semibold text-[#111827] leading-[1.4] whitespace-nowrap overflow-hidden text-ellipsis">Maria Perez shared a file with you</div>
                <div class="text-xs text-[#9ca3af] mt-0.5">1 hour ago</div>
              </div>
            </div>
          </div>
          <button class="absolute right-2 top-1/2 [transform:translateY(-50%)] w-[22px] h-[22px] rounded-full border-0 bg-[#f3f4f6] text-[#9ca3af] cursor-pointer flex items-center justify-center z-[2] [transition:background_0.12s,_color_0.12s] hover:bg-[#fee2e2] hover:text-[#ef4444]" onclick="deleteItem('item-2')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
  
        <li class="list-item relative border-b border-b-[#f3f4f6] overflow-hidden [transition:max-height_0.35s_ease,_opacity_0.25s_ease] max-h-20" id="item-3">
          <div class="swipe-track relative flex items-stretch cursor-grab select-none [touch-action:pan-y]" id="track-3">
            <div class="delete-bg absolute right-0 top-0 bottom-0 bg-[#ef4444] flex items-center gap-1.5 py-0 px-4 text-[#fff] text-[11px] font-bold min-w-[80px] justify-center">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content relative flex items-center gap-[11px] pt-3 pr-9 pb-3 pl-4 bg-[#fff] w-full [transition:transform_0.15s_ease] z-[1]">
              <div class="w-9 h-9 rounded-[10px] flex items-center justify-center text-[11px] font-extrabold text-[#fff] shrink-0" style="background:linear-gradient(135deg,#10b981,#059669)">TN</div>
              <div class="flex-1 min-w-0">
                <div class="text-xs font-semibold text-[#111827] leading-[1.4] whitespace-nowrap overflow-hidden text-ellipsis">Tom Nelson merged your pull request</div>
                <div class="text-xs text-[#9ca3af] mt-0.5">3 hours ago</div>
              </div>
              <div class="item-dot w-[7px] h-[7px] rounded-full shrink-0 unread"></div>
            </div>
          </div>
          <button class="absolute right-2 top-1/2 [transform:translateY(-50%)] w-[22px] h-[22px] rounded-full border-0 bg-[#f3f4f6] text-[#9ca3af] cursor-pointer flex items-center justify-center z-[2] [transition:background_0.12s,_color_0.12s] hover:bg-[#fee2e2] hover:text-[#ef4444]" onclick="deleteItem('item-3')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
  
        <li class="list-item relative border-b border-b-[#f3f4f6] overflow-hidden [transition:max-height_0.35s_ease,_opacity_0.25s_ease] max-h-20" id="item-4">
          <div class="swipe-track relative flex items-stretch cursor-grab select-none [touch-action:pan-y]" id="track-4">
            <div class="delete-bg absolute right-0 top-0 bottom-0 bg-[#ef4444] flex items-center gap-1.5 py-0 px-4 text-[#fff] text-[11px] font-bold min-w-[80px] justify-center">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content relative flex items-center gap-[11px] pt-3 pr-9 pb-3 pl-4 bg-[#fff] w-full [transition:transform_0.15s_ease] z-[1]">
              <div class="w-9 h-9 rounded-[10px] flex items-center justify-center text-[11px] font-extrabold text-[#fff] shrink-0" style="background:linear-gradient(135deg,#ec4899,#f43f5e)">SW</div>
              <div class="flex-1 min-w-0">
                <div class="text-xs font-semibold text-[#111827] leading-[1.4] whitespace-nowrap overflow-hidden text-ellipsis">Sara White invited you to a project</div>
                <div class="text-xs text-[#9ca3af] mt-0.5">Yesterday</div>
              </div>
            </div>
          </div>
          <button class="absolute right-2 top-1/2 [transform:translateY(-50%)] w-[22px] h-[22px] rounded-full border-0 bg-[#f3f4f6] text-[#9ca3af] cursor-pointer flex items-center justify-center z-[2] [transition:background_0.12s,_color_0.12s] hover:bg-[#fee2e2] hover:text-[#ef4444]" onclick="deleteItem('item-4')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
      </ul>
  
      <div class="empty-state flex flex-col items-center justify-center gap-2.5 py-10 px-5" id="emptyState" style="display:none">
        <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#d1d5db" stroke-width="1.5" stroke-linecap="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
        <p>All caught up!</p>
      </div>
    </div>
  </div>
  <script>
    var startX = 0, currentX = 0, activeTrack = null, THRESHOLD = 80;
    
    function pointerDown(e, track) {
      activeTrack = track;
      startX = e.touches ? e.touches[0].clientX : e.clientX;
      currentX = 0;
      track.classList.add('no-transition', 'dragging');
      document.addEventListener('mousemove', pointerMove);
      document.addEventListener('mouseup', pointerUp);
      document.addEventListener('touchmove', pointerMove, { passive: false });
      document.addEventListener('touchend', pointerUp);
    }
    function pointerMove(e) {
      if (!activeTrack) return;
      var x = (e.touches ? e.touches[0].clientX : e.clientX) - startX;
      currentX = Math.min(0, x);
      var content = activeTrack.querySelector('.item-content');
      var reveal = Math.min(Math.abs(currentX), THRESHOLD + 20);
      content.style.transform = 'translateX(' + currentX + 'px)';
      var bg = activeTrack.querySelector('.delete-bg');
      bg.style.opacity = Math.min(1, Math.abs(currentX) / THRESHOLD);
      if (e.cancelable) e.preventDefault();
    }
    function pointerUp() {
      if (!activeTrack) return;
      activeTrack.classList.remove('no-transition', 'dragging');
      var content = activeTrack.querySelector('.item-content');
      if (Math.abs(currentX) >= THRESHOLD) {
        var li = activeTrack.closest('.list-item');
        content.style.transform = 'translateX(-100%)';
        setTimeout(() => deleteItem(li.id), 250);
      } else {
        content.style.transform = '';
        var bg = activeTrack.querySelector('.delete-bg');
        bg.style.opacity = '';
      }
      activeTrack = null;
      document.removeEventListener('mousemove', pointerMove);
      document.removeEventListener('mouseup', pointerUp);
      document.removeEventListener('touchmove', pointerMove);
      document.removeEventListener('touchend', pointerUp);
    }
    
    // Attach swipe handlers
    document.querySelectorAll('.swipe-track').forEach(function(track) {
      track.addEventListener('mousedown', function(e) { pointerDown(e, track); });
      track.addEventListener('touchstart', function(e) { pointerDown(e, track); }, { passive: true });
    });
    
    function deleteItem(id) {
      var li = document.getElementById(id);
      if (!li) return;
      li.classList.add('removing');
      setTimeout(function() {
        li.remove();
        updateBadge();
        checkEmpty();
      }, 380);
    }
    function updateBadge() {
      var count = document.querySelectorAll('.item-dot.unread').length;
      var badge = document.getElementById('badge');
      badge.textContent = count;
      badge.style.display = count > 0 ? '' : 'none';
    }
    function checkEmpty() {
      var items = document.querySelectorAll('.list-item');
      if (items.length === 0) {
        document.getElementById('emptyState').style.display = 'flex';
      }
    }
  </script>
</body>
</html>
React
import React, { useEffect } from 'react';

// CSS — optionally move to SwipeToDeleteList.module.css
const css = `
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f1f5f9; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 20px; }
.demo { width: 100%; max-width: 360px; }
.panel { background: #fff; border-radius: 20px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); overflow: hidden; }
.panel-header { display: flex; align-items: center; gap: 8px; padding: 16px 18px 12px; border-bottom: 1px solid #f3f4f6; }
.panel-title { font-size: 15px; font-weight: 800; color: #111827; }
.badge { background: #ef4444; color: #fff; font-size: 10px; font-weight: 800; padding: 2px 7px; border-radius: 20px; }

/* List */
.list { list-style: none; }
.list-item { position: relative; border-bottom: 1px solid #f3f4f6; overflow: hidden; transition: max-height 0.35s ease, opacity 0.25s ease; max-height: 80px; }
.list-item:last-child { border-bottom: none; }
.list-item.removing { max-height: 0; opacity: 0; }

/* Swipe track */
.swipe-track { position: relative; display: flex; align-items: stretch; cursor: grab; user-select: none; touch-action: pan-y; }
.swipe-track.dragging { cursor: grabbing; }
.delete-bg { position: absolute; right: 0; top: 0; bottom: 0; background: #ef4444; display: flex; align-items: center; gap: 6px; padding: 0 16px; color: #fff; font-size: 11px; font-weight: 700; min-width: 80px; justify-content: center; }
.item-content { position: relative; display: flex; align-items: center; gap: 11px; padding: 12px 36px 12px 16px; background: #fff; width: 100%; transition: transform 0.15s ease; z-index: 1; }
.swipe-track.no-transition .item-content { transition: none; }

/* Item parts */
.item-avatar { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 800; color: #fff; flex-shrink: 0; }
.item-body { flex: 1; min-width: 0; }
.item-title { font-size: 12px; font-weight: 600; color: #111827; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.item-sub { font-size: 10px; color: #9ca3af; margin-top: 2px; }
.item-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.item-dot.unread { background: #3b82f6; }

/* Delete button (always visible on right) */
.del-btn { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); width: 22px; height: 22px; border-radius: 50%; border: none; background: #f3f4f6; color: #9ca3af; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 2; transition: background 0.12s, color 0.12s; }
.del-btn:hover { background: #fee2e2; color: #ef4444; }

/* Empty state */
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 40px 20px; }
.empty-state p { font-size: 13px; color: #9ca3af; font-weight: 600; }
`;

export default function SwipeToDeleteList() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var startX = 0, currentX = 0, activeTrack = null, THRESHOLD = 80;
    
    function pointerDown(e, track) {
      activeTrack = track;
      startX = e.touches ? e.touches[0].clientX : e.clientX;
      currentX = 0;
      track.classList.add('no-transition', 'dragging');
      document.addEventListener('mousemove', pointerMove);
      document.addEventListener('mouseup', pointerUp);
      document.addEventListener('touchmove', pointerMove, { passive: false });
      document.addEventListener('touchend', pointerUp);
    }
    function pointerMove(e) {
      if (!activeTrack) return;
      var x = (e.touches ? e.touches[0].clientX : e.clientX) - startX;
      currentX = Math.min(0, x);
      var content = activeTrack.querySelector('.item-content');
      var reveal = Math.min(Math.abs(currentX), THRESHOLD + 20);
      content.style.transform = 'translateX(' + currentX + 'px)';
      var bg = activeTrack.querySelector('.delete-bg');
      bg.style.opacity = Math.min(1, Math.abs(currentX) / THRESHOLD);
      if (e.cancelable) e.preventDefault();
    }
    function pointerUp() {
      if (!activeTrack) return;
      activeTrack.classList.remove('no-transition', 'dragging');
      var content = activeTrack.querySelector('.item-content');
      if (Math.abs(currentX) >= THRESHOLD) {
        var li = activeTrack.closest('.list-item');
        content.style.transform = 'translateX(-100%)';
        setTimeout(() => deleteItem(li.id), 250);
      } else {
        content.style.transform = '';
        var bg = activeTrack.querySelector('.delete-bg');
        bg.style.opacity = '';
      }
      activeTrack = null;
      document.removeEventListener('mousemove', pointerMove);
      document.removeEventListener('mouseup', pointerUp);
      document.removeEventListener('touchmove', pointerMove);
      document.removeEventListener('touchend', pointerUp);
    }
    
    // Attach swipe handlers
    document.querySelectorAll('.swipe-track').forEach(function(track) {
      track.addEventListener('mousedown', function(e) { pointerDown(e, track); });
      track.addEventListener('touchstart', function(e) { pointerDown(e, track); }, { passive: true });
    });
    
    function deleteItem(id) {
      var li = document.getElementById(id);
      if (!li) return;
      li.classList.add('removing');
      setTimeout(function() {
        li.remove();
        updateBadge();
        checkEmpty();
      }, 380);
    }
    function updateBadge() {
      var count = document.querySelectorAll('.item-dot.unread').length;
      var badge = document.getElementById('badge');
      badge.textContent = count;
      badge.style.display = count > 0 ? '' : 'none';
    }
    function checkEmpty() {
      var items = document.querySelectorAll('.list-item');
      if (items.length === 0) {
        document.getElementById('emptyState').style.display = 'flex';
      }
    }
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };

    // Expose handlers used by inline JSX events to the global scope
    if (typeof deleteItem === 'function') window.deleteItem = deleteItem;
  }, []);

  return (
    <>
      <style>{css}</style>
      <div className="demo">
        <div className="panel">
          <div className="panel-header">
            <h2 className="panel-title">Notifications</h2>
            <span className="badge" id="badge">5</span>
          </div>
      
          <ul className="list" id="list">
            <li className="list-item" id="item-0">
              <div className="swipe-track" id="track-0">
                <div className="delete-bg">
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                  Delete
                </div>
                <div className="item-content">
                  <div className="item-avatar" style={{ background: 'linear-gradient(135deg,#6366f1,#8b5cf6)' }}>JD</div>
                  <div className="item-body">
                    <div className="item-title">Jane Doe liked your post</div>
                    <div className="item-sub">2 minutes ago</div>
                  </div>
                  <div className="item-dot unread"></div>
                </div>
              </div>
              <button className="del-btn" onClick={(event) => { deleteItem('item-0') }} title="Delete">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </li>
      
            <li className="list-item" id="item-1">
              <div className="swipe-track" id="track-1">
                <div className="delete-bg">
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                  Delete
                </div>
                <div className="item-content">
                  <div className="item-avatar" style={{ background: 'linear-gradient(135deg,#0ea5e9,#06b6d4)' }}>AK</div>
                  <div className="item-body">
                    <div className="item-title">Alex Kim commented on your PR</div>
                    <div className="item-sub">15 minutes ago</div>
                  </div>
                  <div className="item-dot unread"></div>
                </div>
              </div>
              <button className="del-btn" onClick={(event) => { deleteItem('item-1') }} title="Delete">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </li>
      
            <li className="list-item" id="item-2">
              <div className="swipe-track" id="track-2">
                <div className="delete-bg">
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                  Delete
                </div>
                <div className="item-content">
                  <div className="item-avatar" style={{ background: 'linear-gradient(135deg,#f59e0b,#ef4444)' }}>MP</div>
                  <div className="item-body">
                    <div className="item-title">Maria Perez shared a file with you</div>
                    <div className="item-sub">1 hour ago</div>
                  </div>
                </div>
              </div>
              <button className="del-btn" onClick={(event) => { deleteItem('item-2') }} title="Delete">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </li>
      
            <li className="list-item" id="item-3">
              <div className="swipe-track" id="track-3">
                <div className="delete-bg">
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                  Delete
                </div>
                <div className="item-content">
                  <div className="item-avatar" style={{ background: 'linear-gradient(135deg,#10b981,#059669)' }}>TN</div>
                  <div className="item-body">
                    <div className="item-title">Tom Nelson merged your pull request</div>
                    <div className="item-sub">3 hours ago</div>
                  </div>
                  <div className="item-dot unread"></div>
                </div>
              </div>
              <button className="del-btn" onClick={(event) => { deleteItem('item-3') }} title="Delete">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </li>
      
            <li className="list-item" id="item-4">
              <div className="swipe-track" id="track-4">
                <div className="delete-bg">
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                  Delete
                </div>
                <div className="item-content">
                  <div className="item-avatar" style={{ background: 'linear-gradient(135deg,#ec4899,#f43f5e)' }}>SW</div>
                  <div className="item-body">
                    <div className="item-title">Sara White invited you to a project</div>
                    <div className="item-sub">Yesterday</div>
                  </div>
                </div>
              </div>
              <button className="del-btn" onClick={(event) => { deleteItem('item-4') }} title="Delete">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </li>
          </ul>
      
          <div className="empty-state" id="emptyState" style={{ display: 'none' }}>
            <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#d1d5db" strokeWidth="1.5" strokeLinecap="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
            <p>All caught up!</p>
          </div>
        </div>
      </div>
    </>
  );
}
React + Tailwind
import React, { useEffect } from 'react';
// Requires Tailwind CSS v3+ — https://tailwindcss.com/docs/installation
// Arbitrary value classes (e.g. bg-[#0f172a]) are valid Tailwind v3+

export default function SwipeToDeleteList() {
  // Auto-generated escape hatch: the original snippet's vanilla JS runs once
  // after mount and queries the rendered DOM. For idiomatic React, lift this
  // into state + handlers.
  useEffect(() => {
    const _listeners = [];
    const _originalAddEventListener = EventTarget.prototype.addEventListener;
    EventTarget.prototype.addEventListener = function(type, listener, options) {
      _listeners.push({ target: this, type, listener, options });
      return _originalAddEventListener.call(this, type, listener, options);
    };
    var startX = 0, currentX = 0, activeTrack = null, THRESHOLD = 80;
    
    function pointerDown(e, track) {
      activeTrack = track;
      startX = e.touches ? e.touches[0].clientX : e.clientX;
      currentX = 0;
      track.classList.add('no-transition', 'dragging');
      document.addEventListener('mousemove', pointerMove);
      document.addEventListener('mouseup', pointerUp);
      document.addEventListener('touchmove', pointerMove, { passive: false });
      document.addEventListener('touchend', pointerUp);
    }
    function pointerMove(e) {
      if (!activeTrack) return;
      var x = (e.touches ? e.touches[0].clientX : e.clientX) - startX;
      currentX = Math.min(0, x);
      var content = activeTrack.querySelector('.item-content');
      var reveal = Math.min(Math.abs(currentX), THRESHOLD + 20);
      content.style.transform = 'translateX(' + currentX + 'px)';
      var bg = activeTrack.querySelector('.delete-bg');
      bg.style.opacity = Math.min(1, Math.abs(currentX) / THRESHOLD);
      if (e.cancelable) e.preventDefault();
    }
    function pointerUp() {
      if (!activeTrack) return;
      activeTrack.classList.remove('no-transition', 'dragging');
      var content = activeTrack.querySelector('.item-content');
      if (Math.abs(currentX) >= THRESHOLD) {
        var li = activeTrack.closest('.list-item');
        content.style.transform = 'translateX(-100%)';
        setTimeout(() => deleteItem(li.id), 250);
      } else {
        content.style.transform = '';
        var bg = activeTrack.querySelector('.delete-bg');
        bg.style.opacity = '';
      }
      activeTrack = null;
      document.removeEventListener('mousemove', pointerMove);
      document.removeEventListener('mouseup', pointerUp);
      document.removeEventListener('touchmove', pointerMove);
      document.removeEventListener('touchend', pointerUp);
    }
    
    // Attach swipe handlers
    document.querySelectorAll('.swipe-track').forEach(function(track) {
      track.addEventListener('mousedown', function(e) { pointerDown(e, track); });
      track.addEventListener('touchstart', function(e) { pointerDown(e, track); }, { passive: true });
    });
    
    function deleteItem(id) {
      var li = document.getElementById(id);
      if (!li) return;
      li.classList.add('removing');
      setTimeout(function() {
        li.remove();
        updateBadge();
        checkEmpty();
      }, 380);
    }
    function updateBadge() {
      var count = document.querySelectorAll('.item-dot.unread').length;
      var badge = document.getElementById('badge');
      badge.textContent = count;
      badge.style.display = count > 0 ? '' : 'none';
    }
    function checkEmpty() {
      var items = document.querySelectorAll('.list-item');
      if (items.length === 0) {
        document.getElementById('emptyState').style.display = 'flex';
      }
    }
    // Cleanup: restore addEventListener and remove all listeners
    return () => {
      EventTarget.prototype.addEventListener = _originalAddEventListener;
      for (const { target, type, listener, options } of _listeners) {
        target.removeEventListener(type, listener, options);
      }
    };

    // Expose handlers used by inline JSX events to the global scope
    if (typeof deleteItem === 'function') window.deleteItem = deleteItem;
  }, []);

  return (
    <>
      <style>{`
* {
  box-sizing: border-box; margin: 0; padding: 0;
}

body {
  font-family: system-ui, sans-serif; background: #f1f5f9; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 20px;
}

.badge {
  background: #ef4444; color: #fff; font-size: 10px; font-weight: 800; padding: 2px 7px; border-radius: 20px;
}

.list-item:last-child {
  border-bottom: none;
}

.list-item.removing {
  max-height: 0; opacity: 0;
}

.swipe-track.dragging {
  cursor: grabbing;
}

.swipe-track.no-transition .item-content {
  transition: none;
}

.item-dot.unread {
  background: #3b82f6;
}

.empty-state p {
  font-size: 13px; color: #9ca3af; font-weight: 600;
}
      `}</style>
      <div className="w-full max-w-[360px]">
        <div className="bg-[#fff] rounded-[20px] shadow-[0_8px_32px_rgba(0,0,0,0.1)] overflow-hidden">
          <div className="flex items-center gap-2 pt-4 px-[18px] pb-3 border-b border-b-[#f3f4f6]">
            <h2 className="text-[15px] font-extrabold text-[#111827]">Notifications</h2>
            <span className="badge" id="badge">5</span>
          </div>
      
          <ul className="[list-style:none]" id="list">
            <li className="list-item relative border-b border-b-[#f3f4f6] overflow-hidden [transition:max-height_0.35s_ease,_opacity_0.25s_ease] max-h-20" id="item-0">
              <div className="swipe-track relative flex items-stretch cursor-grab select-none [touch-action:pan-y]" id="track-0">
                <div className="delete-bg absolute right-0 top-0 bottom-0 bg-[#ef4444] flex items-center gap-1.5 py-0 px-4 text-[#fff] text-[11px] font-bold min-w-[80px] justify-center">
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                  Delete
                </div>
                <div className="item-content relative flex items-center gap-[11px] pt-3 pr-9 pb-3 pl-4 bg-[#fff] w-full [transition:transform_0.15s_ease] z-[1]">
                  <div className="w-9 h-9 rounded-[10px] flex items-center justify-center text-[11px] font-extrabold text-[#fff] shrink-0" style={{ background: 'linear-gradient(135deg,#6366f1,#8b5cf6)' }}>JD</div>
                  <div className="flex-1 min-w-0">
                    <div className="text-xs font-semibold text-[#111827] leading-[1.4] whitespace-nowrap overflow-hidden text-ellipsis">Jane Doe liked your post</div>
                    <div className="text-xs text-[#9ca3af] mt-0.5">2 minutes ago</div>
                  </div>
                  <div className="item-dot w-[7px] h-[7px] rounded-full shrink-0 unread"></div>
                </div>
              </div>
              <button className="absolute right-2 top-1/2 [transform:translateY(-50%)] w-[22px] h-[22px] rounded-full border-0 bg-[#f3f4f6] text-[#9ca3af] cursor-pointer flex items-center justify-center z-[2] [transition:background_0.12s,_color_0.12s] hover:bg-[#fee2e2] hover:text-[#ef4444]" onClick={(event) => { deleteItem('item-0') }} title="Delete">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </li>
      
            <li className="list-item relative border-b border-b-[#f3f4f6] overflow-hidden [transition:max-height_0.35s_ease,_opacity_0.25s_ease] max-h-20" id="item-1">
              <div className="swipe-track relative flex items-stretch cursor-grab select-none [touch-action:pan-y]" id="track-1">
                <div className="delete-bg absolute right-0 top-0 bottom-0 bg-[#ef4444] flex items-center gap-1.5 py-0 px-4 text-[#fff] text-[11px] font-bold min-w-[80px] justify-center">
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                  Delete
                </div>
                <div className="item-content relative flex items-center gap-[11px] pt-3 pr-9 pb-3 pl-4 bg-[#fff] w-full [transition:transform_0.15s_ease] z-[1]">
                  <div className="w-9 h-9 rounded-[10px] flex items-center justify-center text-[11px] font-extrabold text-[#fff] shrink-0" style={{ background: 'linear-gradient(135deg,#0ea5e9,#06b6d4)' }}>AK</div>
                  <div className="flex-1 min-w-0">
                    <div className="text-xs font-semibold text-[#111827] leading-[1.4] whitespace-nowrap overflow-hidden text-ellipsis">Alex Kim commented on your PR</div>
                    <div className="text-xs text-[#9ca3af] mt-0.5">15 minutes ago</div>
                  </div>
                  <div className="item-dot w-[7px] h-[7px] rounded-full shrink-0 unread"></div>
                </div>
              </div>
              <button className="absolute right-2 top-1/2 [transform:translateY(-50%)] w-[22px] h-[22px] rounded-full border-0 bg-[#f3f4f6] text-[#9ca3af] cursor-pointer flex items-center justify-center z-[2] [transition:background_0.12s,_color_0.12s] hover:bg-[#fee2e2] hover:text-[#ef4444]" onClick={(event) => { deleteItem('item-1') }} title="Delete">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </li>
      
            <li className="list-item relative border-b border-b-[#f3f4f6] overflow-hidden [transition:max-height_0.35s_ease,_opacity_0.25s_ease] max-h-20" id="item-2">
              <div className="swipe-track relative flex items-stretch cursor-grab select-none [touch-action:pan-y]" id="track-2">
                <div className="delete-bg absolute right-0 top-0 bottom-0 bg-[#ef4444] flex items-center gap-1.5 py-0 px-4 text-[#fff] text-[11px] font-bold min-w-[80px] justify-center">
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                  Delete
                </div>
                <div className="item-content relative flex items-center gap-[11px] pt-3 pr-9 pb-3 pl-4 bg-[#fff] w-full [transition:transform_0.15s_ease] z-[1]">
                  <div className="w-9 h-9 rounded-[10px] flex items-center justify-center text-[11px] font-extrabold text-[#fff] shrink-0" style={{ background: 'linear-gradient(135deg,#f59e0b,#ef4444)' }}>MP</div>
                  <div className="flex-1 min-w-0">
                    <div className="text-xs font-semibold text-[#111827] leading-[1.4] whitespace-nowrap overflow-hidden text-ellipsis">Maria Perez shared a file with you</div>
                    <div className="text-xs text-[#9ca3af] mt-0.5">1 hour ago</div>
                  </div>
                </div>
              </div>
              <button className="absolute right-2 top-1/2 [transform:translateY(-50%)] w-[22px] h-[22px] rounded-full border-0 bg-[#f3f4f6] text-[#9ca3af] cursor-pointer flex items-center justify-center z-[2] [transition:background_0.12s,_color_0.12s] hover:bg-[#fee2e2] hover:text-[#ef4444]" onClick={(event) => { deleteItem('item-2') }} title="Delete">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </li>
      
            <li className="list-item relative border-b border-b-[#f3f4f6] overflow-hidden [transition:max-height_0.35s_ease,_opacity_0.25s_ease] max-h-20" id="item-3">
              <div className="swipe-track relative flex items-stretch cursor-grab select-none [touch-action:pan-y]" id="track-3">
                <div className="delete-bg absolute right-0 top-0 bottom-0 bg-[#ef4444] flex items-center gap-1.5 py-0 px-4 text-[#fff] text-[11px] font-bold min-w-[80px] justify-center">
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                  Delete
                </div>
                <div className="item-content relative flex items-center gap-[11px] pt-3 pr-9 pb-3 pl-4 bg-[#fff] w-full [transition:transform_0.15s_ease] z-[1]">
                  <div className="w-9 h-9 rounded-[10px] flex items-center justify-center text-[11px] font-extrabold text-[#fff] shrink-0" style={{ background: 'linear-gradient(135deg,#10b981,#059669)' }}>TN</div>
                  <div className="flex-1 min-w-0">
                    <div className="text-xs font-semibold text-[#111827] leading-[1.4] whitespace-nowrap overflow-hidden text-ellipsis">Tom Nelson merged your pull request</div>
                    <div className="text-xs text-[#9ca3af] mt-0.5">3 hours ago</div>
                  </div>
                  <div className="item-dot w-[7px] h-[7px] rounded-full shrink-0 unread"></div>
                </div>
              </div>
              <button className="absolute right-2 top-1/2 [transform:translateY(-50%)] w-[22px] h-[22px] rounded-full border-0 bg-[#f3f4f6] text-[#9ca3af] cursor-pointer flex items-center justify-center z-[2] [transition:background_0.12s,_color_0.12s] hover:bg-[#fee2e2] hover:text-[#ef4444]" onClick={(event) => { deleteItem('item-3') }} title="Delete">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </li>
      
            <li className="list-item relative border-b border-b-[#f3f4f6] overflow-hidden [transition:max-height_0.35s_ease,_opacity_0.25s_ease] max-h-20" id="item-4">
              <div className="swipe-track relative flex items-stretch cursor-grab select-none [touch-action:pan-y]" id="track-4">
                <div className="delete-bg absolute right-0 top-0 bottom-0 bg-[#ef4444] flex items-center gap-1.5 py-0 px-4 text-[#fff] text-[11px] font-bold min-w-[80px] justify-center">
                  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" strokeWidth="2.5" strokeLinecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                  Delete
                </div>
                <div className="item-content relative flex items-center gap-[11px] pt-3 pr-9 pb-3 pl-4 bg-[#fff] w-full [transition:transform_0.15s_ease] z-[1]">
                  <div className="w-9 h-9 rounded-[10px] flex items-center justify-center text-[11px] font-extrabold text-[#fff] shrink-0" style={{ background: 'linear-gradient(135deg,#ec4899,#f43f5e)' }}>SW</div>
                  <div className="flex-1 min-w-0">
                    <div className="text-xs font-semibold text-[#111827] leading-[1.4] whitespace-nowrap overflow-hidden text-ellipsis">Sara White invited you to a project</div>
                    <div className="text-xs text-[#9ca3af] mt-0.5">Yesterday</div>
                  </div>
                </div>
              </div>
              <button className="absolute right-2 top-1/2 [transform:translateY(-50%)] w-[22px] h-[22px] rounded-full border-0 bg-[#f3f4f6] text-[#9ca3af] cursor-pointer flex items-center justify-center z-[2] [transition:background_0.12s,_color_0.12s] hover:bg-[#fee2e2] hover:text-[#ef4444]" onClick={(event) => { deleteItem('item-4') }} title="Delete">
                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
              </button>
            </li>
          </ul>
      
          <div className="empty-state flex flex-col items-center justify-center gap-2.5 py-10 px-5" id="emptyState" style={{ display: 'none' }}>
            <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#d1d5db" strokeWidth="1.5" strokeLinecap="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
            <p>All caught up!</p>
          </div>
        </div>
      </div>
    </>
  );
}
Vue
<template>
  <div class="demo">
    <div class="panel">
      <div class="panel-header">
        <h2 class="panel-title">Notifications</h2>
        <span class="badge" id="badge">5</span>
      </div>
  
      <ul class="list" id="list">
        <li class="list-item" id="item-0">
          <div class="swipe-track" id="track-0">
            <div class="delete-bg">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content">
              <div class="item-avatar" style="background:linear-gradient(135deg,#6366f1,#8b5cf6)">JD</div>
              <div class="item-body">
                <div class="item-title">Jane Doe liked your post</div>
                <div class="item-sub">2 minutes ago</div>
              </div>
              <div class="item-dot unread"></div>
            </div>
          </div>
          <button class="del-btn" @click="deleteItem('item-0')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
  
        <li class="list-item" id="item-1">
          <div class="swipe-track" id="track-1">
            <div class="delete-bg">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content">
              <div class="item-avatar" style="background:linear-gradient(135deg,#0ea5e9,#06b6d4)">AK</div>
              <div class="item-body">
                <div class="item-title">Alex Kim commented on your PR</div>
                <div class="item-sub">15 minutes ago</div>
              </div>
              <div class="item-dot unread"></div>
            </div>
          </div>
          <button class="del-btn" @click="deleteItem('item-1')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
  
        <li class="list-item" id="item-2">
          <div class="swipe-track" id="track-2">
            <div class="delete-bg">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content">
              <div class="item-avatar" style="background:linear-gradient(135deg,#f59e0b,#ef4444)">MP</div>
              <div class="item-body">
                <div class="item-title">Maria Perez shared a file with you</div>
                <div class="item-sub">1 hour ago</div>
              </div>
            </div>
          </div>
          <button class="del-btn" @click="deleteItem('item-2')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
  
        <li class="list-item" id="item-3">
          <div class="swipe-track" id="track-3">
            <div class="delete-bg">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content">
              <div class="item-avatar" style="background:linear-gradient(135deg,#10b981,#059669)">TN</div>
              <div class="item-body">
                <div class="item-title">Tom Nelson merged your pull request</div>
                <div class="item-sub">3 hours ago</div>
              </div>
              <div class="item-dot unread"></div>
            </div>
          </div>
          <button class="del-btn" @click="deleteItem('item-3')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
  
        <li class="list-item" id="item-4">
          <div class="swipe-track" id="track-4">
            <div class="delete-bg">
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
              Delete
            </div>
            <div class="item-content">
              <div class="item-avatar" style="background:linear-gradient(135deg,#ec4899,#f43f5e)">SW</div>
              <div class="item-body">
                <div class="item-title">Sara White invited you to a project</div>
                <div class="item-sub">Yesterday</div>
              </div>
            </div>
          </div>
          <button class="del-btn" @click="deleteItem('item-4')" title="Delete">
            <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </li>
      </ul>
  
      <div class="empty-state" id="emptyState" style="display:none">
        <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#d1d5db" stroke-width="1.5" stroke-linecap="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
        <p>All caught up!</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { onMounted } from 'vue';

var startX = 0, currentX = 0, activeTrack = null, THRESHOLD = 80;
function pointerDown(e, track) {
  activeTrack = track;
  startX = e.touches ? e.touches[0].clientX : e.clientX;
  currentX = 0;
  track.classList.add('no-transition', 'dragging');
  document.addEventListener('mousemove', pointerMove);
  document.addEventListener('mouseup', pointerUp);
  document.addEventListener('touchmove', pointerMove, { passive: false });
  document.addEventListener('touchend', pointerUp);
}
function pointerMove(e) {
  if (!activeTrack) return;
  var x = (e.touches ? e.touches[0].clientX : e.clientX) - startX;
  currentX = Math.min(0, x);
  var content = activeTrack.querySelector('.item-content');
  var reveal = Math.min(Math.abs(currentX), THRESHOLD + 20);
  content.style.transform = 'translateX(' + currentX + 'px)';
  var bg = activeTrack.querySelector('.delete-bg');
  bg.style.opacity = Math.min(1, Math.abs(currentX) / THRESHOLD);
  if (e.cancelable) e.preventDefault();
}
function pointerUp() {
  if (!activeTrack) return;
  activeTrack.classList.remove('no-transition', 'dragging');
  var content = activeTrack.querySelector('.item-content');
  if (Math.abs(currentX) >= THRESHOLD) {
    var li = activeTrack.closest('.list-item');
    content.style.transform = 'translateX(-100%)';
    setTimeout(() => deleteItem(li.id), 250);
  } else {
    content.style.transform = '';
    var bg = activeTrack.querySelector('.delete-bg');
    bg.style.opacity = '';
  }
  activeTrack = null;
  document.removeEventListener('mousemove', pointerMove);
  document.removeEventListener('mouseup', pointerUp);
  document.removeEventListener('touchmove', pointerMove);
  document.removeEventListener('touchend', pointerUp);
}
function deleteItem(id) {
  var li = document.getElementById(id);
  if (!li) return;
  li.classList.add('removing');
  setTimeout(function() {
    li.remove();
    updateBadge();
    checkEmpty();
  }, 380);
}
function updateBadge() {
  var count = document.querySelectorAll('.item-dot.unread').length;
  var badge = document.getElementById('badge');
  badge.textContent = count;
  badge.style.display = count > 0 ? '' : 'none';
}
function checkEmpty() {
  var items = document.querySelectorAll('.list-item');
  if (items.length === 0) {
    document.getElementById('emptyState').style.display = 'flex';
  }
}

onMounted(() => {
  // Attach swipe handlers
  document.querySelectorAll('.swipe-track').forEach(function(track) {
    track.addEventListener('mousedown', function(e) { pointerDown(e, track); });
    track.addEventListener('touchstart', function(e) { pointerDown(e, track); }, { passive: true });
  });
});
</script>

<style scoped>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f1f5f9; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 20px; }
.demo { width: 100%; max-width: 360px; }
.panel { background: #fff; border-radius: 20px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); overflow: hidden; }
.panel-header { display: flex; align-items: center; gap: 8px; padding: 16px 18px 12px; border-bottom: 1px solid #f3f4f6; }
.panel-title { font-size: 15px; font-weight: 800; color: #111827; }
.badge { background: #ef4444; color: #fff; font-size: 10px; font-weight: 800; padding: 2px 7px; border-radius: 20px; }

/* List */
.list { list-style: none; }
.list-item { position: relative; border-bottom: 1px solid #f3f4f6; overflow: hidden; transition: max-height 0.35s ease, opacity 0.25s ease; max-height: 80px; }
.list-item:last-child { border-bottom: none; }
.list-item.removing { max-height: 0; opacity: 0; }

/* Swipe track */
.swipe-track { position: relative; display: flex; align-items: stretch; cursor: grab; user-select: none; touch-action: pan-y; }
.swipe-track.dragging { cursor: grabbing; }
.delete-bg { position: absolute; right: 0; top: 0; bottom: 0; background: #ef4444; display: flex; align-items: center; gap: 6px; padding: 0 16px; color: #fff; font-size: 11px; font-weight: 700; min-width: 80px; justify-content: center; }
.item-content { position: relative; display: flex; align-items: center; gap: 11px; padding: 12px 36px 12px 16px; background: #fff; width: 100%; transition: transform 0.15s ease; z-index: 1; }
.swipe-track.no-transition .item-content { transition: none; }

/* Item parts */
.item-avatar { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 800; color: #fff; flex-shrink: 0; }
.item-body { flex: 1; min-width: 0; }
.item-title { font-size: 12px; font-weight: 600; color: #111827; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.item-sub { font-size: 10px; color: #9ca3af; margin-top: 2px; }
.item-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.item-dot.unread { background: #3b82f6; }

/* Delete button (always visible on right) */
.del-btn { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); width: 22px; height: 22px; border-radius: 50%; border: none; background: #f3f4f6; color: #9ca3af; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 2; transition: background 0.12s, color 0.12s; }
.del-btn:hover { background: #fee2e2; color: #ef4444; }

/* Empty state */
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 40px 20px; }
.empty-state p { font-size: 13px; color: #9ca3af; font-weight: 600; }
</style>
Angular
// @ts-nocheck
// Note: vanilla JS DOM manipulation is preserved as-is inside ngAfterViewInit().
// For idiomatic Angular, replace document.getElementById() with @ViewChild() refs
// and move state into component properties with two-way binding.
import { Component, AfterViewInit, ViewEncapsulation } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-swipe-to-delete-list',
  standalone: true,
  imports: [CommonModule],
  encapsulation: ViewEncapsulation.None,
  template: `
    <div class="demo">
      <div class="panel">
        <div class="panel-header">
          <h2 class="panel-title">Notifications</h2>
          <span class="badge" id="badge">5</span>
        </div>
    
        <ul class="list" id="list">
          <li class="list-item" id="item-0">
            <div class="swipe-track" id="track-0">
              <div class="delete-bg">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                Delete
              </div>
              <div class="item-content">
                <div class="item-avatar" style="background:linear-gradient(135deg,#6366f1,#8b5cf6)">JD</div>
                <div class="item-body">
                  <div class="item-title">Jane Doe liked your post</div>
                  <div class="item-sub">2 minutes ago</div>
                </div>
                <div class="item-dot unread"></div>
              </div>
            </div>
            <button class="del-btn" (click)="deleteItem('item-0')" title="Delete">
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
            </button>
          </li>
    
          <li class="list-item" id="item-1">
            <div class="swipe-track" id="track-1">
              <div class="delete-bg">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                Delete
              </div>
              <div class="item-content">
                <div class="item-avatar" style="background:linear-gradient(135deg,#0ea5e9,#06b6d4)">AK</div>
                <div class="item-body">
                  <div class="item-title">Alex Kim commented on your PR</div>
                  <div class="item-sub">15 minutes ago</div>
                </div>
                <div class="item-dot unread"></div>
              </div>
            </div>
            <button class="del-btn" (click)="deleteItem('item-1')" title="Delete">
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
            </button>
          </li>
    
          <li class="list-item" id="item-2">
            <div class="swipe-track" id="track-2">
              <div class="delete-bg">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                Delete
              </div>
              <div class="item-content">
                <div class="item-avatar" style="background:linear-gradient(135deg,#f59e0b,#ef4444)">MP</div>
                <div class="item-body">
                  <div class="item-title">Maria Perez shared a file with you</div>
                  <div class="item-sub">1 hour ago</div>
                </div>
              </div>
            </div>
            <button class="del-btn" (click)="deleteItem('item-2')" title="Delete">
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
            </button>
          </li>
    
          <li class="list-item" id="item-3">
            <div class="swipe-track" id="track-3">
              <div class="delete-bg">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                Delete
              </div>
              <div class="item-content">
                <div class="item-avatar" style="background:linear-gradient(135deg,#10b981,#059669)">TN</div>
                <div class="item-body">
                  <div class="item-title">Tom Nelson merged your pull request</div>
                  <div class="item-sub">3 hours ago</div>
                </div>
                <div class="item-dot unread"></div>
              </div>
            </div>
            <button class="del-btn" (click)="deleteItem('item-3')" title="Delete">
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
            </button>
          </li>
    
          <li class="list-item" id="item-4">
            <div class="swipe-track" id="track-4">
              <div class="delete-bg">
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2.5" stroke-linecap="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
                Delete
              </div>
              <div class="item-content">
                <div class="item-avatar" style="background:linear-gradient(135deg,#ec4899,#f43f5e)">SW</div>
                <div class="item-body">
                  <div class="item-title">Sara White invited you to a project</div>
                  <div class="item-sub">Yesterday</div>
                </div>
              </div>
            </div>
            <button class="del-btn" (click)="deleteItem('item-4')" title="Delete">
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
            </button>
          </li>
        </ul>
    
        <div class="empty-state" id="emptyState" style="display:none">
          <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#d1d5db" stroke-width="1.5" stroke-linecap="round"><path d="M18 8h1a4 4 0 0 1 0 8h-1"/><path d="M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="1" x2="6" y2="4"/><line x1="10" y1="1" x2="10" y2="4"/><line x1="14" y1="1" x2="14" y2="4"/></svg>
          <p>All caught up!</p>
        </div>
      </div>
    </div>
  `,
  styles: [`
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: system-ui, sans-serif; background: #f1f5f9; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 20px; }
    .demo { width: 100%; max-width: 360px; }
    .panel { background: #fff; border-radius: 20px; box-shadow: 0 8px 32px rgba(0,0,0,0.1); overflow: hidden; }
    .panel-header { display: flex; align-items: center; gap: 8px; padding: 16px 18px 12px; border-bottom: 1px solid #f3f4f6; }
    .panel-title { font-size: 15px; font-weight: 800; color: #111827; }
    .badge { background: #ef4444; color: #fff; font-size: 10px; font-weight: 800; padding: 2px 7px; border-radius: 20px; }
    
    /* List */
    .list { list-style: none; }
    .list-item { position: relative; border-bottom: 1px solid #f3f4f6; overflow: hidden; transition: max-height 0.35s ease, opacity 0.25s ease; max-height: 80px; }
    .list-item:last-child { border-bottom: none; }
    .list-item.removing { max-height: 0; opacity: 0; }
    
    /* Swipe track */
    .swipe-track { position: relative; display: flex; align-items: stretch; cursor: grab; user-select: none; touch-action: pan-y; }
    .swipe-track.dragging { cursor: grabbing; }
    .delete-bg { position: absolute; right: 0; top: 0; bottom: 0; background: #ef4444; display: flex; align-items: center; gap: 6px; padding: 0 16px; color: #fff; font-size: 11px; font-weight: 700; min-width: 80px; justify-content: center; }
    .item-content { position: relative; display: flex; align-items: center; gap: 11px; padding: 12px 36px 12px 16px; background: #fff; width: 100%; transition: transform 0.15s ease; z-index: 1; }
    .swipe-track.no-transition .item-content { transition: none; }
    
    /* Item parts */
    .item-avatar { width: 36px; height: 36px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 800; color: #fff; flex-shrink: 0; }
    .item-body { flex: 1; min-width: 0; }
    .item-title { font-size: 12px; font-weight: 600; color: #111827; line-height: 1.4; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .item-sub { font-size: 10px; color: #9ca3af; margin-top: 2px; }
    .item-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
    .item-dot.unread { background: #3b82f6; }
    
    /* Delete button (always visible on right) */
    .del-btn { position: absolute; right: 8px; top: 50%; transform: translateY(-50%); width: 22px; height: 22px; border-radius: 50%; border: none; background: #f3f4f6; color: #9ca3af; cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 2; transition: background 0.12s, color 0.12s; }
    .del-btn:hover { background: #fee2e2; color: #ef4444; }
    
    /* Empty state */
    .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; padding: 40px 20px; }
    .empty-state p { font-size: 13px; color: #9ca3af; font-weight: 600; }
  `]
})
export class SwipeToDeleteListComponent implements AfterViewInit {
  ngAfterViewInit(): void {
    var startX = 0, currentX = 0, activeTrack = null, THRESHOLD = 80;
    
    function pointerDown(e, track) {
      activeTrack = track;
      startX = e.touches ? e.touches[0].clientX : e.clientX;
      currentX = 0;
      track.classList.add('no-transition', 'dragging');
      document.addEventListener('mousemove', pointerMove);
      document.addEventListener('mouseup', pointerUp);
      document.addEventListener('touchmove', pointerMove, { passive: false });
      document.addEventListener('touchend', pointerUp);
    }
    function pointerMove(e) {
      if (!activeTrack) return;
      var x = (e.touches ? e.touches[0].clientX : e.clientX) - startX;
      currentX = Math.min(0, x);
      var content = activeTrack.querySelector('.item-content');
      var reveal = Math.min(Math.abs(currentX), THRESHOLD + 20);
      content.style.transform = 'translateX(' + currentX + 'px)';
      var bg = activeTrack.querySelector('.delete-bg');
      bg.style.opacity = Math.min(1, Math.abs(currentX) / THRESHOLD);
      if (e.cancelable) e.preventDefault();
    }
    function pointerUp() {
      if (!activeTrack) return;
      activeTrack.classList.remove('no-transition', 'dragging');
      var content = activeTrack.querySelector('.item-content');
      if (Math.abs(currentX) >= THRESHOLD) {
        var li = activeTrack.closest('.list-item');
        content.style.transform = 'translateX(-100%)';
        setTimeout(() => deleteItem(li.id), 250);
      } else {
        content.style.transform = '';
        var bg = activeTrack.querySelector('.delete-bg');
        bg.style.opacity = '';
      }
      activeTrack = null;
      document.removeEventListener('mousemove', pointerMove);
      document.removeEventListener('mouseup', pointerUp);
      document.removeEventListener('touchmove', pointerMove);
      document.removeEventListener('touchend', pointerUp);
    }
    
    // Attach swipe handlers
    document.querySelectorAll('.swipe-track').forEach(function(track) {
      track.addEventListener('mousedown', function(e) { pointerDown(e, track); });
      track.addEventListener('touchstart', function(e) { pointerDown(e, track); }, { passive: true });
    });
    
    function deleteItem(id) {
      var li = document.getElementById(id);
      if (!li) return;
      li.classList.add('removing');
      setTimeout(function() {
        li.remove();
        updateBadge();
        checkEmpty();
      }, 380);
    }
    function updateBadge() {
      var count = document.querySelectorAll('.item-dot.unread').length;
      var badge = document.getElementById('badge');
      badge.textContent = count;
      badge.style.display = count > 0 ? '' : 'none';
    }
    function checkEmpty() {
      var items = document.querySelectorAll('.list-item');
      if (items.length === 0) {
        document.getElementById('emptyState').style.display = 'flex';
      }
    }

    // Bind inline-handler functions to the component so the template can call them
    Object.assign(this, { pointerDown, pointerMove, pointerUp, deleteItem, updateBadge, checkEmpty });
  }
}