<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);
});