A dynamic card swipe carousel with smooth transitions and visual effects.
Sponsored by: Java "The" Script
| <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); | |
| } |