GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 Swipe to Delete List UI Patterns (Copy-Paste)

Swipe to delete is dragging a list row sideways so a delete button slides out from behind it — the app gesture you keep wanting on the web.

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine aren't ranked by popularity — they follow the order a list actually goes through. First come one-row gestures (01-02 swipe, 03 pull to refresh), then whole-list operations (04-05 reordering and the sortable table, 06 pinned viewing), and finally what a list needs once it gets too long to show at once (07 loading, 08 bulk selection, 09 paging). Tapping a row to expand it belongs to 9 Accordion List UI Patterns, and comparing two photos by a draggable handle belongs to 9 Before After Slider UIs.

01Swipe-to-delete reveal

What moves

Dragging a row left reveals a red delete button behind it, and dragging past halfway auto-dismisses the whole row.

Swipe-to-delete reveal
Open in a new tab

01 · Swipe-to-delete reveal — Measured area 6.517% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointerdown/move/uptransform:translateX임계값 스냅

In the code

The button is a real, always-present <button>, so tabbing to the row and pressing Delete works just as well as a finger.

// pointermove sets --dx directly, then two thresholds decide what happens on release
content.addEventListener('pointermove', function (e) {
  dx = Math.min(0, Math.max(-170, e.clientX - startX));
  content.style.transform = 'translateX(' + dx + 'px)';
});
function end() {
  if (dx < -140) { removeRow(); }                 // past the delete threshold
  else content.style.transform = dx < -44 ? 'translateX(-88px)' : '';
}

02Swipe multi-action stack

What moves

The further you drag, the more buttons — star, archive, and delete — pop in from the side and stack up in order.

Swipe multi-action stack
Open in a new tab

02 · Swipe multi-action stack — Measured area 12.767% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

stagger revealtransform 순차 지연touch-action: pan-y

In the code

All three are real buttons, so tabbing to the row reveals them for keyboard users too.

.action {
  opacity: 0;
  transform: translateY(8px);
  transition: opacity .22s, transform .22s;
}
// keyboard focus landing on the row (not just a hover) reveals the same three buttons
.row:focus-within .action { opacity: 1; transform: translateY(0); }
.action--fav     { animation-delay: 0ms; }
.action--archive { animation-delay: 80ms; }
.action--del     { animation-delay: 160ms; }

03Pull-to-refresh spinner

What moves

Dragging down from the top of the list stretches with rubber-band resistance, then releases into a spinning refresh.

Pull-to-refresh spinner
Open in a new tab

03 · Pull-to-refresh spinner — Measured area 14.518% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

touch 이동 거리elastic ease-outspinner rotate keyframes

In the code

A visible refresh button triggers the exact same animation for anyone who can't perform the drag.

function end() {
  if (dy > 44) refresh();            // pulled past the halfway mark
  else body.style.transform = '';    // otherwise it springs back
}
function refresh() {
  body.style.transform = 'translateY(56px)';
  setTimeout(function () { body.style.transform = ''; }, 900);
}

04Drag-handle reorder

What moves

Grabbing the six-dot handle on the left and dragging up or down nudges the rows it passes aside and reorders the list.

Drag-handle reorder
Open in a new tab

04 · Drag-handle reorder — Measured area 18.702% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointerdown 드래그형제 요소 reflowdrop index 계산

In the code

Without a pointer at all, focusing the handle and pressing the arrow keys reorders it the same way.

handle.addEventListener('keydown', function (e) {
  var sib = e.key === 'ArrowDown' ? row.nextElementSibling
          : e.key === 'ArrowUp'   ? row.previousElementSibling : null;
  if (!sib) return;
  e.preventDefault();
  e.key === 'ArrowDown' ? list.insertBefore(sib, row) : list.insertBefore(row, sib);
  handle.focus({ preventScroll: true });   // keeps the parent page from jumping under an iframe
});

05Click-to-sort header

What moves

Clicking a column header flips its arrow and re-sorts the rows by that column, toggling ascending and descending.

Click-to-sort header
Open in a new tab

05 · Click-to-sort header — Measured area 3.665% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

aria-sort 속성click 토글화살표 rotate transform

In the code

The current state is exposed through aria-sort, so a screen reader announces it the same way a sighted user sees the arrow flip.

btn.addEventListener('click', function () {
  var dir = btn.getAttribute('aria-sort') === 'ascending' ? 'descending' : 'ascending';
  ths.forEach(function (b) { b.setAttribute('aria-sort', 'none'); });
  btn.setAttribute('aria-sort', dir);
  rows.sort(function (a, b) {
    var cmp = a[key] > b[key] ? 1 : a[key] < b[key] ? -1 : 0;
    return dir === 'ascending' ? cmp : -cmp;
  }).forEach(function (r) { tbody.appendChild(r); });
});

06Frozen header + first column

What moves

Scroll the table in any direction and the top header row plus the leftmost column stay pinned in place.

Frozen header + first column
Open in a new tab

06 · Frozen header + first column — Measured area 11.599% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

position:sticky top+leftz-index 겹침 순서border 겹선 보정

In the code

position: sticky gets both a top and a left, and only the one cell where both apply needs a higher z-index.

// header row — stays put while the table scrolls vertically
thead th {
  position: sticky;
  top: 0;
  z-index: 2;
}
// first column — stays put while the table scrolls horizontally
tbody th[scope="row"] {
  position: sticky;
  left: 0;
  z-index: 1;
}
// the one cell where both apply needs to sit above both layers
.corner { position: sticky; top: 0; left: 0; z-index: 3; }

07Skeleton shimmer rows

What moves

Before data arrives, gray bar rows sweep a light shimmer left to right, previewing where content will land. Keeping the sweep's highlight far lighter than its base color is what makes the motion actually read in a screenshot or a gif.

Skeleton shimmer rows
Open in a new tab

07 · Skeleton shimmer rows — Measured area 4.918% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

shimmer gradient sweeparia-busybackground-position keyframes

In the code

A toggle button swaps the skeleton for loaded content on demand, so you can compare both states side by side.

.bar {
  background: linear-gradient(90deg, #d8d8de 25%, rgba(255, 255, 255, .95) 50%, #d8d8de 75%);
  background-size: 200% 100%;
  animation: shimmer 1.4s linear infinite;
}
@keyframes shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

08Bulk-select toolbar

What moves

Checking even one row checkbox slides up a bottom toolbar with bulk actions like delete and move.

Bulk-select toolbar
Open in a new tab

08 · Bulk-select toolbar — Measured area 6.635% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · See the password

indeterminate checkboxtranslateY(120%)selectall 동기화

In the code

When only some rows are checked, the select-all box switches to an indeterminate state to show it isn't "all or nothing."

function sync() {
  var n = items.filter(function (c) { return c.checked; }).length;
  all.checked = n === items.length;
  all.indeterminate = n > 0 && n < items.length;    // neither none nor all are checked
  list.classList.toggle('has-selection', n > 0);    // this is what slides the toolbar up
}

09Numbered pagination

What moves

Page-number buttons sit in a row, and once there are too many, the middle collapses into an ellipsis with only the current page highlighted.

Numbered pagination
Open in a new tab

09 · Numbered pagination — Measured area 4.884% · moving frames 10/23Original by GODRICH · SCSS and React ship in the zip · See the password

current page stateellipsis collapse 계산prev/next disabled

In the code

This demo runs on eight pages, so only the first, the last, and whatever sits next to the current one keeps its own number.

function pages(total, current) {
  var out = [];
  for (var p = 1; p <= total; p++) {
    // keep a number for the first page, the last page, or a neighbor of the current one
    if (p === 1 || p === total || Math.abs(p - current) <= 1) {
      out.push(p);
    } else if (out[out.length - 1] !== '...') {
      out.push('...');           // a run of skipped numbers collapses into one dot
    }
  }
  return out;
}

Where list swipe UI breaks

pull to refresh — where does it break?

Items 01 and 02 both call setPointerCapture the instant a drag starts. Skip that one line (an easy thing to forget) and a finger sliding just outside the button's edge drops the pointermove stream, so the swipe stalls halfway with no obvious reason why. The second trap turned up while building item 08's toolbar, which is supposed to sit off-screen until a row gets checked: re-rendering this same 480x300 demo at a 320px width caught it pushing document.documentElement.scrollHeight past the viewport before the list around it was wrapped in overflow: hidden — that wrapper is what ships in this zip now. Scaling that drag-to-reorder feel up to a whole screen is what Portfolio Gallery HTML CSS covers. The zip holding all nine files opens with kjj4ftra, and once it's unzipped, the vanilla and React folders sit side by side for comparison.

Accessibility — what reduced-motion does to each of the nine

What we checked

Under prefers-reduced-motion: reduce, items 01 through 05 and 08 keep transition: none and drop only the timing, while item 06 swaps its smooth scroll for scroll-behavior: auto — either way, a swipe, a sort, or a pin still happens exactly as before, just instantly instead of eased. Item 07 (skeleton) is handled differently: instead of killing the shimmer outright, its duration stretches to 3.2 seconds, so the loading signal stays but the flicker that can bother some viewers is cut way down. Item 09 (pagination) only ever had a color transition to begin with, so there's nothing further to strip out. Every item keeps its focus outline, so Tab and the arrow keys alone can reach and operate all nine without a mouse.

Where to go next

More list and table patterns live in the Real-world UI category, and what this site actually is gets explained on the about page. The scroll-container rules behind position: sticky are documented on MDN, and the pointer-capture API behind items 01 and 02 is covered in the MDN Pointer events docs.

FAQ — Frequently Asked Questions

Does swipe to delete or css skeleton loading really work without a JavaScript library?

Yes. Items 01 and 02 here are built from three events — pointerdown, pointermove, and pointerup — plus transform: translateX(), with no separate gesture library involved.

Why transform instead of top/left for dragging?

Changing top, left, or height forces the browser to recompute layout on every single frame, which is where dragging stutters. Moving only transform and opacity updates a composited layer instead, with no layout recalculation at all.

Does the vanilla code differ from the React version?

No — every value driving the movement matches. The vanilla demo loops nonstop under an is-demo class so the gallery has something to show, while the React components only respond to real pointer and click events through useState and useRef. Its table sibling — columns you drag wider, cells you select and copy — is 9 Data Table UI Patterns.

Enter the archive password

The password is inside this article. You will find it as you read.