About this snippet

Add a mobile-friendly swipeable card stack to your app. We completely redesigned the touch interactions, so you get a buttery smooth swiping experience on any device.

Size
6.2 KB
Uses
HTML, CSS, JavaScript
Loads
placehold.co
Views
273
<div class="swipe-app">
  <div class="stack-container" id="card-stack">
    <!-- Bottom Card -->
    <div class="swipe-card">
      <img src="https://placehold.co/320x450/8b5cf6/ffffff?text=Card+Three" alt="Card Three">
      <div class="card-info">
        <h3>Mountain Retreat</h3>
        <p>Peaceful and quiet vibes</p>
      </div>
    </div>
    <!-- Middle Card -->
    <div class="swipe-card">
      <img src="https://placehold.co/320x450/ec4899/ffffff?text=Card+Two" alt="Card Two">
      <div class="card-info">
        <h3>City Lights</h3>
        <p>The city that never sleeps</p>
      </div>
    </div>
    <!-- Top Card -->
    <div class="swipe-card">
      <img src="https://placehold.co/320x450/3b82f6/ffffff?text=Card+One" alt="Card One">
      <div class="card-info">
        <h3>Ocean Breeze</h3>
        <p>Listen to the calming waves</p>
      </div>
    </div>
  </div>
  
  <div class="action-buttons">
    <button id="btn-nope" class="btn-action btn-nope" aria-label="Reject">
      <svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
        <line x1="18" y1="6" x2="6" y2="18"></line>
        <line x1="6" y1="6" x2="18" y2="18"></line>
      </svg>
    </button>
    <button id="btn-yep" class="btn-action btn-yep" aria-label="Accept">
      <svg viewBox="0 0 24 24" width="32" height="32" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
        <polyline points="20 6 9 17 4 12"></polyline>
      </svg>
    </button>
  </div>
</div>
*, *::before, *::after {
  box-sizing: border-box;
}

body {
  font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
  background: #f1f5f9;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  margin: 0;
  overflow: hidden;
}

.swipe-app {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
}

.stack-container {
  position: relative;
  width: 320px;
  height: 450px;
  /* Prevents native mobile page scrolling when dragging the cards */
  touch-action: none;
}

.swipe-card {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: #ffffff;
  border-radius: 24px;
  box-shadow: 0 15px 25px -5px rgba(0, 0, 0, 0.15);
  overflow: hidden;
  will-change: transform;
  transform-origin: 50% 100%;
  cursor: grab;
  user-select: none;
}

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

/* Dynamic stacking trick: targets elements from bottom to top */
.swipe-card:nth-last-child(1) {
  transform: translateY(0px) scale(1);
  z-index: 3;
}

.swipe-card:nth-last-child(2) {
  transform: translateY(16px) scale(0.95);
  z-index: 2;
  opacity: 0.9;
}

.swipe-card:nth-last-child(3) {
  transform: translateY(32px) scale(0.9);
  z-index: 1;
  opacity: 0.8;
}

.swipe-card img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Prevents native drag ghosting on desktop */
  pointer-events: none;
}

.card-info {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  padding: 40px 24px 24px 24px;
  background: linear-gradient(to top, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0) 100%);
  color: #ffffff;
  pointer-events: none;
}

.card-info h3 {
  margin: 0 0 4px 0;
  font-size: 1.75rem;
  letter-spacing: -0.5px;
}

.card-info p {
  margin: 0;
  font-size: 1rem;
  color: #cbd5e1;
}

.action-buttons {
  display: flex;
  justify-content: center;
  gap: 24px;
  margin-top: 50px;
  z-index: 10;
}

.btn-action {
  width: 72px;
  height: 72px;
  border-radius: 50%;
  border: none;
  background: #ffffff;
  display: flex;
  justify-content: center;
  align-items: center;
  box-shadow: 0 10px 20px -5px rgba(0, 0, 0, 0.1);
  cursor: pointer;
  transition: transform 0.2s cubic-bezier(0.34, 1.56, 0.64, 1), box-shadow 0.2s;
}

.btn-action:active {
  transform: scale(0.85);
  box-shadow: 0 4px 10px -2px rgba(0, 0, 0, 0.1);
}

.btn-nope {
  color: #ef4444;
}

.btn-yep {
  color: #10b981;
}
const stack = document.getElementById("card-stack");
const btnNope = document.getElementById("btn-nope");
const btnYep = document.getElementById("btn-yep");

let isDragging = false;
let startX = 0;
let currentX = 0;
let activeCard = null;

// Pointer events handle both mouse and touch flawlessly
stack.addEventListener("pointerdown", function(e) {
  // Always grab the top-most card in the DOM
  activeCard = stack.lastElementChild;
  if (!activeCard) return;
  
  isDragging = true;
  startX = e.clientX;
  
  activeCard.classList.add("dragging");
  activeCard.style.transition = "none";
  activeCard.setPointerCapture(e.pointerId);
});

stack.addEventListener("pointermove", function(e) {
  if (!isDragging || !activeCard) return;
  
  currentX = e.clientX - startX;
  // Calculate rotation based on drag distance
  const rotate = currentX * 0.05;
  
  activeCard.style.transform = "translate(" + currentX + "px, 0px) rotate(" + rotate + "deg)";
});

function endDrag(e) {
  if (!isDragging || !activeCard) return;
  isDragging = false;
  
  activeCard.classList.remove("dragging");
  activeCard.style.transition = "transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1)";
  
  // If dragged far enough, swipe it away
  if (Math.abs(currentX) > 100) {
    const direction = currentX > 0 ? 1 : -1;
    swipeOut(activeCard, direction);
  } else {
    // Snap back to center
    activeCard.style.transform = "translate(0px, 0px) rotate(0deg)";
  }
  
  activeCard = null;
  currentX = 0;
}

stack.addEventListener("pointerup", endDrag);
stack.addEventListener("pointercancel", endDrag);

// Helper function to animate card out and remove it
function swipeOut(card, direction) {
  const throwX = window.innerWidth * direction;
  const throwRotate = direction * 30;
  
  card.style.transition = "transform 0.5s ease-out";
  card.style.transform = "translate(" + throwX + "px, 0px) rotate(" + throwRotate + "deg)";
  
  setTimeout(function() {
    if (card.parentElement) {
      card.parentElement.removeChild(card);
    }
  }, 500);
}

// Button controls
btnNope.addEventListener("click", function() {
  const topCard = stack.lastElementChild;
  if (topCard) swipeOut(topCard, -1);
});

btnYep.addEventListener("click", function() {
  const topCard = stack.lastElementChild;
  if (topCard) swipeOut(topCard, 1);
});
Esc
↑↓ moveEnter select

Start from a template

Open an .html file, or HTML, CSS, JS, Markdown, Less or TypeScript files. You can also drop them here. Styles and scripts go into their panes.

Share

Source

Keyboard shortcuts

Save