Back to CSS Stacked Cards Swipe Stack CSS + JS
Share
HTML
<div class="scd-swipe">
  <div class="scd-swipe__stage" data-scd-swipe="stage">
    <div class="scd-swipe__card scd-swipe__card--b4"><h3>Maya</h3><p>Loves slow mornings &amp; film cameras</p></div>
    <div class="scd-swipe__card scd-swipe__card--b3"><h3>Theo</h3><p>Trail runner, terrible cook</p></div>
    <div class="scd-swipe__card scd-swipe__card--b2"><h3>Lena</h3><p>Vinyl collector since &#39;09</p></div>
    <div class="scd-swipe__card scd-swipe__card--b1"><h3>Drag me</h3><p>Swipe left or right &rarr;</p></div>
    <div class="scd-swipe__hint">drag the top card</div>
  </div>
</div>
CSS
@import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@600;800&display=swap');

.scd-swipe, .scd-swipe *, .scd-swipe *::before, .scd-swipe *::after { box-sizing: border-box; margin: 0; padding: 0; }

.scd-swipe {
  min-height: 460px;
  display: grid;
  place-items: center;
  background: linear-gradient(160deg,#fdeff9,#ec38bc 120%);
  font-family: 'Plus Jakarta Sans', sans-serif;
}
.scd-swipe__stage { position: relative; width: 260px; height: 360px; }
.scd-swipe__card {
  position: absolute; inset: 0;
  border-radius: 26px;
  color: #fff;
  box-shadow: 0 20px 44px rgba(120,30,90,.35);
  display: flex; flex-direction: column; justify-content: flex-end;
  padding: 28px;
  cursor: grab; user-select: none;
  transition: transform .4s cubic-bezier(.3,.9,.3,1);
}
.scd-swipe__card.is-dragging { transition: none; cursor: grabbing; }
.scd-swipe__card h3 { font-size: 1.9rem; font-weight: 800; }
.scd-swipe__card p { opacity: .9; }
.scd-swipe__card--b1 { background: linear-gradient(150deg,#fa709a,#fee140); }
.scd-swipe__card--b2 { background: linear-gradient(150deg,#a18cd1,#fbc2eb); color: #5a2a6a; }
.scd-swipe__card--b3 { background: linear-gradient(150deg,#43cea2,#185a9d); }
.scd-swipe__card--b4 { background: linear-gradient(150deg,#ff6e7f,#bfe9ff); color: #3a3a5a; }
.scd-swipe__hint {
  position: absolute; bottom: -36px; width: 100%;
  text-align: center; color: #fff; opacity: .8; font-size: .85rem;
}

@media (prefers-reduced-motion: reduce) {
  .scd-swipe__card { transition: none !important; }
}
JS
(() => {
  const root = document.querySelector('.scd-swipe');
  if (!root) return;
  const deck = root.querySelector('[data-scd-swipe="stage"]');
  if (!deck) return;

  function layout() {
    const cards = [...deck.querySelectorAll('.scd-swipe__card')];
    cards.forEach((c, i) => {
      const d = cards.length - 1 - i;
      c.style.transform = `translateY(${d * -10}px) scale(${1 - d * 0.04})`;
      c.style.zIndex = i;
    });
  }
  layout();

  function bind() {
    const top = [...deck.querySelectorAll('.scd-swipe__card')].pop();
    if (!top) return;
    let sx = 0, dx = 0, drag = false;
    const down = (e) => {
      drag = true;
      sx = (e.touches ? e.touches[0].clientX : e.clientX);
      top.classList.add('is-dragging');
    };
    const move = (e) => {
      if (!drag) return;
      dx = (e.touches ? e.touches[0].clientX : e.clientX) - sx;
      top.style.transform = `translateX(${dx}px) rotate(${dx * 0.06}deg)`;
    };
    const up = () => {
      if (!drag) return;
      drag = false;
      top.classList.remove('is-dragging');
      if (Math.abs(dx) > 110) {
        top.style.transform = `translateX(${dx > 0 ? 600 : -600}px) rotate(${dx > 0 ? 40 : -40}deg)`;
        top.style.opacity = 0;
        setTimeout(() => { top.remove(); layout(); bind(); }, 300);
      } else {
        top.style.transform = '';
        layout();
      }
      dx = 0;
    };
    top.addEventListener('mousedown', down);
    root.addEventListener('mousemove', move);
    root.addEventListener('mouseup', up);
    root.addEventListener('mouseleave', up);
    top.addEventListener('touchstart', down);
    root.addEventListener('touchmove', move);
    root.addEventListener('touchend', up);
  }
  bind();
})();