Swipe away
Drag a card sideways. Past a threshold it flies off screen; otherwise it springs back.
CSSJavaScriptIntermediate
<article class="card" id="c"><h2>Swipe me</h2><p>Drag left or right, then let go.</p></article>
<p class="hint">Drag the card</p>
body{overflow:hidden}
.card{width:min(300px,84vw);padding:28px;border-radius:16px;background:linear-gradient(145deg,#3a2220,#1f0f0e);border:1px solid #5a3a35;
touch-action:pan-y;cursor:grab;user-select:none;will-change:transform}
.card.settle{transition:transform .45s cubic-bezier(.2,.9,.25,1.2),opacity .45s}
.card h2{margin:0 0 6px}.card p{margin:0;color:#cfc3c1}
.hint{position:fixed;bottom:18px;color:#a6938f;font-size:13px}
const c = document.getElementById('c');
let startX = 0, dx = 0, dragging = false;
c.addEventListener('pointerdown', e => {
dragging = true; startX = e.clientX;
c.classList.remove('settle');
c.style.transition = '';
c.setPointerCapture(e.pointerId);
});
c.addEventListener('pointermove', e => {
if (!dragging) return;
dx = e.clientX - startX;
c.style.transform = `translateX(${dx}px) rotate(${dx / 18}deg)`;
c.style.opacity = 1 - Math.min(Math.abs(dx) / 400, .6);
});
c.addEventListener('pointerup', () => {
dragging = false;
c.classList.add('settle');
if (Math.abs(dx) > 110) { // past the threshold: leave
const dir = Math.sign(dx);
c.style.transform = `translateX(${dir * 520}px) rotate(${dir * 24}deg)`;
c.style.opacity = 0;
setTimeout(comeBack, 900);
} else { // not far enough: spring back
c.style.transform = '';
c.style.opacity = 1;
}
dx = 0;
});
function comeBack() { // demo only: bring the card back
c.classList.remove('settle');
c.style.transition = 'none';
c.style.transform = 'translateY(30px)';
requestAnimationFrame(() => requestAnimationFrame(() => {
c.style.transition = 'transform .6s cubic-bezier(.2,.8,.2,1), opacity .6s';
c.style.transform = '';
c.style.opacity = 1;
}));
}
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Swipe away</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{display:grid;place-items:center;background:#140807;color:#fffffc;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
body{overflow:hidden}
.card{width:min(300px,84vw);padding:28px;border-radius:16px;background:linear-gradient(145deg,#3a2220,#1f0f0e);border:1px solid #5a3a35;
touch-action:pan-y;cursor:grab;user-select:none;will-change:transform}
.card.settle{transition:transform .45s cubic-bezier(.2,.9,.25,1.2),opacity .45s}
.card h2{margin:0 0 6px}.card p{margin:0;color:#cfc3c1}
.hint{position:fixed;bottom:18px;color:#a6938f;font-size:13px}
</style></head>
<body>
<article class="card" id="c"><h2>Swipe me</h2><p>Drag left or right, then let go.</p></article>
<p class="hint">Drag the card</p>
<script>
const c = document.getElementById('c');
let startX = 0, dx = 0, dragging = false;
c.addEventListener('pointerdown', e => {
dragging = true; startX = e.clientX;
c.classList.remove('settle');
c.style.transition = '';
c.setPointerCapture(e.pointerId);
});
c.addEventListener('pointermove', e => {
if (!dragging) return;
dx = e.clientX - startX;
c.style.transform = `translateX(${dx}px) rotate(${dx / 18}deg)`;
c.style.opacity = 1 - Math.min(Math.abs(dx) / 400, .6);
});
c.addEventListener('pointerup', () => {
dragging = false;
c.classList.add('settle');
if (Math.abs(dx) > 110) { // past the threshold: leave
const dir = Math.sign(dx);
c.style.transform = `translateX(${dir * 520}px) rotate(${dir * 24}deg)`;
c.style.opacity = 0;
setTimeout(comeBack, 900);
} else { // not far enough: spring back
c.style.transform = '';
c.style.opacity = 1;
}
dx = 0;
});
function comeBack() { // demo only: bring the card back
c.classList.remove('settle');
c.style.transition = 'none';
c.style.transform = 'translateY(30px)';
requestAnimationFrame(() => requestAnimationFrame(() => {
c.style.transition = 'transform .6s cubic-bezier(.2,.8,.2,1), opacity .6s';
c.style.transform = '';
c.style.opacity = 1;
}));
}
</script>
</body></html>
Good for
Mobile notifications, inbox rows, tinder-style cards and bottom sheets.
Watch out for
Use pointer events plus touch-action pan-y so vertical page scrolling still works.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.