Skip to content

Instantly share code, notes, and snippets.

@tolepcoy
Created July 19, 2025 16:38
Show Gist options
  • Save tolepcoy/5e4702a2d8df66c1873a2a1a5f5f9532 to your computer and use it in GitHub Desktop.
Save tolepcoy/5e4702a2d8df66c1873a2a1a5f5f9532 to your computer and use it in GitHub Desktop.
Card Swipe Carousel
<section class="card-stack">
<article class="card a" style="--i: 0">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-android" viewBox="0 0 16 16">
<path d="M2.76 3.061a.5.5 0 0 1 .679.2l1.283 2.352A8.9 8.9 0 0 1 8 5a8.9 8.9 0 0 1 3.278.613l1.283-2.352a.5.5 0 1 1 .878.478l-1.252 2.295C14.475 7.266 16 9.477 16 12H0c0-2.523 1.525-4.734 3.813-5.966L2.56 3.74a.5.5 0 0 1 .2-.678ZM5 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2m6 0a1 1 0 1 0 0-2 1 1 0 0 0 0 2" />
</svg>
</span>
</article>
<article class="card b" style="--i: 1">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-apple" viewBox="0 0 16 16">
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516s1.52.087 2.475-1.258.762-2.391.728-2.43m3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422s1.675-2.789 1.698-2.854-.597-.79-1.254-1.157a3.7 3.7 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56s.625 1.924 1.273 2.796c.576.984 1.34 1.667 1.659 1.899s1.219.386 1.843.067c.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758q.52-1.185.473-1.282" />
<path d="M11.182.008C11.148-.03 9.923.023 8.857 1.18c-1.066 1.156-.902 2.482-.878 2.516s1.52.087 2.475-1.258.762-2.391.728-2.43m3.314 11.733c-.048-.096-2.325-1.234-2.113-3.422s1.675-2.789 1.698-2.854-.597-.79-1.254-1.157a3.7 3.7 0 0 0-1.563-.434c-.108-.003-.483-.095-1.254.116-.508.139-1.653.589-1.968.607-.316.018-1.256-.522-2.267-.665-.647-.125-1.333.131-1.824.328-.49.196-1.422.754-2.074 2.237-.652 1.482-.311 3.83-.067 4.56s.625 1.924 1.273 2.796c.576.984 1.34 1.667 1.659 1.899s1.219.386 1.843.067c.502-.308 1.408-.485 1.766-.472.357.013 1.061.154 1.782.539.571.197 1.111.115 1.652-.105.541-.221 1.324-1.059 2.238-2.758q.52-1.185.473-1.282" />
</svg>
</span>
</article>
<article class="card c" style="--i: 2;">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-behance" viewBox="0 0 16 16">
<path d="M4.654 3c.461 0 .887.035 1.278.14.39.07.711.216.996.391s.497.426.641.747c.14.32.216.711.216 1.137 0 .496-.106.922-.356 1.242-.215.32-.566.606-.997.817.606.176 1.067.496 1.348.922s.461.957.461 1.563c0 .496-.105.922-.285 1.278a2.3 2.3 0 0 1-.782.887c-.32.215-.711.39-1.137.496a5.3 5.3 0 0 1-1.278.176L0 12.803V3zm-.285 3.978c.39 0 .71-.105.957-.285.246-.18.355-.497.355-.887 0-.216-.035-.426-.105-.567a1 1 0 0 0-.32-.355 1.8 1.8 0 0 0-.461-.176c-.176-.035-.356-.035-.567-.035H2.17v2.31c0-.005 2.2-.005 2.2-.005zm.105 4.193c.215 0 .426-.035.606-.07.176-.035.356-.106.496-.216s.25-.215.356-.39c.07-.176.14-.391.14-.641 0-.496-.14-.852-.426-1.102-.285-.215-.676-.32-1.137-.32H2.17v2.734h2.305zm6.858-.035q.428.427 1.278.426c.39 0 .746-.106 1.032-.286q.426-.32.53-.64h1.74c-.286.851-.712 1.457-1.278 1.848-.566.355-1.243.566-2.06.566a4.1 4.1 0 0 1-1.527-.285 2.8 2.8 0 0 1-1.137-.782 2.85 2.85 0 0 1-.712-1.172c-.175-.461-.25-.957-.25-1.528 0-.531.07-1.032.25-1.493.18-.46.426-.852.747-1.207.32-.32.711-.606 1.137-.782a4 4 0 0 1 1.493-.285c.606 0 1.137.105 1.598.355.46.25.817.532 1.102.958.285.39.496.851.641 1.348.07.496.105.996.07 1.563h-5.15c0 .58.21 1.11.496 1.396m2.24-3.732c-.25-.25-.642-.391-1.103-.391-.32 0-.566.07-.781.176s-.356.25-.496.39a.96.96 0 0 0-.25.497c-.036.175-.07.32-.07.46h3.196c-.07-.526-.25-.882-.497-1.132zm-3.127-3.728h3.978v.957h-3.978z" />
</svg>
</span>
</article>
<article class="card d" style="--i: 3;">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-browser-firefox" viewBox="0 0 16 16">
<path d="M13.384 3.408c.535.276 1.22 1.152 1.556 1.963a8 8 0 0 1 .503 3.897l-.009.077-.026.224A7.758 7.758 0 0 1 .006 8.257v-.04q.025-.545.114-1.082c.01-.074.075-.42.09-.489l.01-.051a6.6 6.6 0 0 1 1.041-2.35q.327-.465.725-.87.35-.358.758-.65a1.5 1.5 0 0 1 .26-.137c-.018.268-.04 1.553.268 1.943h.003a5.7 5.7 0 0 1 1.868-1.443 3.6 3.6 0 0 0 .021 1.896q.105.07.2.152c.107.09.226.207.454.433l.068.066.009.009a2 2 0 0 0 .213.18c.383.287.943.563 1.306.741.201.1.342.168.359.193l.004.008c-.012.193-.695.858-.933.858-2.206 0-2.564 1.335-2.564 1.335.087.997.714 1.839 1.517 2.357a4 4 0 0 0 .439.241q.114.05.228.094c.325.115.665.18 1.01.194 3.043.143 4.155-2.804 3.129-4.745v-.001a3 3 0 0 0-.731-.9 3 3 0 0 0-.571-.37l-.003-.002a2.68 2.68 0 0 1 1.87.454 3.92 3.92 0 0 0-3.396-1.983q-.116.001-.23.01l-.042.003V4.31h-.002a4 4 0 0 0-.8.14 7 7 0 0 0-.333-.314 2 2 0 0 0-.2-.152 4 4 0 0 1-.088-.383 5 5 0 0 1 1.352-.289l.05-.003c.052-.004.125-.01.205-.012C7.996 2.212 8.733.843 10.17.002l-.003.005.003-.001.002-.002h.002l.002-.002h.015a.02.02 0 0 1 .012.007 2.4 2.4 0 0 0 .206.48q.09.153.183.297c.49.774 1.023 1.379 1.543 1.968.771.874 1.512 1.715 2.036 3.02l-.001-.013a8 8 0 0 0-.786-2.353" />
</svg>
</span>
</article>
<article class="card e" style="--i: 4;">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-browser-edge" viewBox="0 0 16 16">
<path d="M9.482 9.341c-.069.062-.17.153-.17.309 0 .162.107.325.3.456.877.613 2.521.54 2.592.538h.002c.667 0 1.32-.18 1.894-.519A3.84 3.84 0 0 0 16 6.819c.018-1.316-.44-2.218-.666-2.664l-.04-.08C13.963 1.487 11.106 0 8 0A8 8 0 0 0 .473 5.29C1.488 4.048 3.183 3.262 5 3.262c2.83 0 5.01 1.885 5.01 4.797h-.004v.002c0 .338-.168.832-.487 1.244l.006-.006z" />
<path d="M.01 7.753a8.14 8.14 0 0 0 .753 3.641 8 8 0 0 0 6.495 4.564 5 5 0 0 1-.785-.377h-.01l-.12-.075a5.5 5.5 0 0 1-1.56-1.463A5.543 5.543 0 0 1 6.81 5.8l.01-.004.025-.012c.208-.098.62-.292 1.167-.285q.194.001.384.033a4 4 0 0 0-.993-.698l-.01-.005C6.348 4.282 5.199 4.263 5 4.263c-2.44 0-4.824 1.634-4.99 3.49m10.263 7.912q.133-.04.265-.084-.153.047-.307.086z" />
<path d="M10.228 15.667a5 5 0 0 0 .303-.086l.082-.025a8.02 8.02 0 0 0 4.162-3.3.25.25 0 0 0-.331-.35q-.322.168-.663.294a6.4 6.4 0 0 1-2.243.4c-2.957 0-5.532-2.031-5.532-4.644q.003-.203.046-.399a4.54 4.54 0 0 0-.46 5.898l.003.005c.315.441.707.821 1.158 1.121h.003l.144.09c.877.55 1.721 1.078 3.328.996" />
</svg>
</span>
</article>
<article class="card f" style="--i: 5">
<span class="icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-browser-chrome" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M16 8a8 8 0 0 1-7.022 7.94l1.902-7.098a3 3 0 0 0 .05-1.492A3 3 0 0 0 10.237 6h5.511A8 8 0 0 1 16 8M0 8a8 8 0 0 0 7.927 8l1.426-5.321a3 3 0 0 1-.723.255 3 3 0 0 1-1.743-.147 3 3 0 0 1-1.043-.7L.633 4.876A8 8 0 0 0 0 8m5.004-.167L1.108 3.936A8.003 8.003 0 0 1 15.418 5H8.066a3 3 0 0 0-1.252.243 2.99 2.99 0 0 0-1.81 2.59M8 10a2 2 0 1 0 0-4 2 2 0 0 0 0 4" />
</svg>
</span>
</article>
</section>
document.addEventListener("DOMContentLoaded", () => {
const cardStack = document.querySelector(".card-stack");
let cards = [...document.querySelectorAll(".card")];
let isSwiping = false;
let startX = 0;
let currentX = 0;
let animationFrameId = null;
const getDurationFromCSS = (
variableName,
element = document.documentElement
) => {
const value = getComputedStyle(element)
?.getPropertyValue(variableName)
?.trim();
if (!value) return 0;
if (value.endsWith("ms")) return parseFloat(value);
if (value.endsWith("s")) return parseFloat(value) * 1000;
return parseFloat(value) || 0;
};
const getActiveCard = () => cards[0];
const updatePositions = () => {
cards.forEach((card, i) => {
card.style.setProperty("--i", i + 1);
card.style.setProperty("--swipe-x", "0px");
card.style.setProperty("--swipe-rotate", "0deg");
card.style.opacity = "1";
});
};
const applySwipeStyles = (deltaX) => {
const card = getActiveCard();
if (!card) return;
card.style.setProperty("--swipe-x", `${deltaX}px`);
card.style.setProperty("--swipe-rotate", `${deltaX * 0.2}deg`);
card.style.opacity = 1 - Math.min(Math.abs(deltaX) / 100, 1) * 0.75;
};
const handleStart = (clientX) => {
if (isSwiping) return;
isSwiping = true;
startX = currentX = clientX;
const card = getActiveCard();
card && (card.style.transition = "none");
};
const handleMove = (clientX) => {
if (!isSwiping) return;
cancelAnimationFrame(animationFrameId);
animationFrameId = requestAnimationFrame(() => {
currentX = clientX;
const deltaX = currentX - startX;
applySwipeStyles(deltaX);
if (Math.abs(deltaX) > 50) handleEnd();
});
};
const handleEnd = () => {
if (!isSwiping) return;
cancelAnimationFrame(animationFrameId);
const deltaX = currentX - startX;
const threshold = 50;
const duration = getDurationFromCSS("--card-swap-duration");
const card = getActiveCard();
if (card) {
card.style.transition = `transform ${duration}ms ease, opacity ${duration}ms ease`;
if (Math.abs(deltaX) > threshold) {
const direction = Math.sign(deltaX);
card.style.setProperty("--swipe-x", `${direction * 300}px`);
card.style.setProperty("--swipe-rotate", `${direction * 20}deg`);
setTimeout(() => {
card.style.setProperty("--swipe-rotate", `${-direction * 20}deg`);
}, duration * 0.5);
setTimeout(() => {
cards = [...cards.slice(1), card];
updatePositions();
}, duration);
} else {
applySwipeStyles(0);
}
}
isSwiping = false;
startX = currentX = 0;
};
const addEventListeners = () => {
cardStack?.addEventListener("pointerdown", ({ clientX }) =>
handleStart(clientX)
);
cardStack?.addEventListener("pointermove", ({ clientX }) =>
handleMove(clientX)
);
cardStack?.addEventListener("pointerup", handleEnd);
};
updatePositions();
addEventListeners();
});
*,
*::after,
*::before {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
display: grid;
place-content: center;
min-block-size: 100vh;
overflow-x: clip;
background: linear-gradient(45deg, hsl(203 7 89), hsl(198 13 71));
}
.card-stack {
width: 16rem;
height: 22rem;
position: relative;
display: grid;
grid-auto-flow: column;
place-content: center;
user-select: none;
touch-action: none;
transform-style: preserve-3d;
}
:root {
--card-perspective: 700px;
--card-z-offset: 12px;
--card-y-offset: 7px;
--card-max-z-index: 100;
--card-swap-duration: 0.3s;
--swipe-x: 0px;
--swipe-rotate: 0deg;
}
.card {
cursor: grab;
background-color: #eee;
display: grid;
place-content: center;
place-self: center;
position: absolute;
width: calc(100% - 2rem);
height: calc(100% - 2rem);
border: 1px solid #99a;
border-radius: 0.75rem;
z-index: calc(var(--card-max-z-index) - var(--i));
transform: perspective(var(--card-perspective))
translateZ(calc(-1 * var(--card-z-offset) * var(--i)))
translateY(calc(var(--card-y-offset) * var(--i)))
translateX(var(--swipe-x, 0px)) rotateY(var(--swipe-rotate, 0deg));
transition: transform var(--card-swap-duration) ease;
will-change: transform;
box-shadow: 0 2px 2px #0003;
}
.icon {
aspect-ratio: 1;
block-size: 6em;
place-self: center;
svg {
display: block;
width: 100%;
height: 100%;
fill: #fff;
filter: drop-shadow(0px 2px 3px #0007);
}
}
.card:active {
cursor: grabbing;
}
.a {
background: linear-gradient(45deg, #32de84, #deb);
}
.b {
background: linear-gradient(45deg, #cf8bf3, #fdb99b);
}
.c {
background: linear-gradient(45deg, #ea52ca, #8ed5f0);
}
.d {
background: linear-gradient(45deg, #967edf, #89ffe3);
}
.e {
background: linear-gradient(45deg, #4ecde2, #faffd2);
}
.f {
background: linear-gradient(45deg, #a4ffbd, #ffd89b);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment