UI Components Medium
Card Automatic Transition
An animated card stack that automatically transitions between cards with smooth scroll-triggered animations, blur effects, and diagonal wipe reveals.
Open in Lab
MCP
html css javascript
Targets: JS HTML
Code
:root {
color-scheme: only light;
--bg: #07070c;
--bg-2: #101225;
--fg: #f5f4f2;
--muted: rgba(245, 244, 242, 0.7);
--accent: #73d0ff;
--ease: cubic-bezier(0.22, 1, 0.36, 1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: "Rubik", "Segoe UI", sans-serif;
background: radial-gradient(900px 520px at 10% -10%, rgba(115, 208, 255, 0.25), transparent 60%),
radial-gradient(900px 620px at 90% 0%, rgba(245, 123, 180, 0.2), transparent 60%),
linear-gradient(150deg, #05050b 0%, #0f1224 55%, #1a1e36 100%);
color: var(--fg);
}
.page {
min-height: 100vh;
}
.hero {
padding: 72px clamp(24px, 5vw, 96px) 40px;
max-width: 760px;
}
.kicker {
text-transform: uppercase;
letter-spacing: 0.38em;
font-size: 0.8rem;
color: var(--accent);
margin: 0 0 16px;
}
.hero h1 {
font-family: "Cardo", "Times New Roman", serif;
font-size: clamp(2.6rem, 5vw, 4.4rem);
margin: 0 0 18px;
}
.lead {
margin: 0;
font-size: 1.1rem;
line-height: 1.7;
color: var(--muted);
}
.scroll-stage {
position: relative;
height: calc((var(--card-count, 4) + 1) * 100vh);
}
.card-stack {
position: sticky;
top: 0;
height: 100vh;
display: grid;
place-items: center;
padding: clamp(24px, 4vw, 52px);
}
.card {
position: absolute;
width: min(960px, 92vw);
min-height: 430px;
border-radius: 32px;
overflow: hidden;
background: rgba(16, 18, 30, 0.78);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 30px 90px rgba(5, 6, 12, 0.55);
display: grid;
grid-template-columns: minmax(240px, 320px) 1fr;
gap: clamp(16px, 3vw, 36px);
padding: clamp(24px, 4vw, 44px);
opacity: 0;
transform: translateX(80px) scale(0.96);
filter: blur(10px);
clip-path: polygon(0 0, 0 100%, 0 100%, 0 0);
transition: opacity 0.12s ease, transform 0.12s ease, filter 0.12s ease;
}
.card::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(115deg, rgba(115, 208, 255, 0.22), transparent 55%);
pointer-events: none;
}
.media {
position: relative;
border-radius: 22px;
min-height: 240px;
background-size: cover;
background-position: center;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12);
transform: translateX(0px) scale(1);
transition: transform 0.2s var(--ease);
}
.media-sky {
background-image: radial-gradient(circle at 15% 20%, rgba(120, 230, 255, 0.9), transparent 55%),
radial-gradient(circle at 80% 30%, rgba(120, 255, 198, 0.6), transparent 55%),
linear-gradient(135deg, #1b2a4a, #263e74);
}
.media-tide {
background-image: radial-gradient(circle at 20% 20%, rgba(255, 192, 118, 0.9), transparent 55%),
radial-gradient(circle at 75% 40%, rgba(255, 108, 150, 0.7), transparent 60%),
linear-gradient(135deg, #2a1c2a, #4a2b3b);
}
.media-dune {
background-image: radial-gradient(circle at 30% 25%, rgba(255, 210, 130, 0.95), transparent 60%),
radial-gradient(circle at 70% 65%, rgba(255, 160, 90, 0.6), transparent 60%),
linear-gradient(140deg, #3b2616, #6a3b1e);
}
.media-orbit {
background-image: radial-gradient(circle at 20% 25%, rgba(160, 185, 255, 0.9), transparent 55%),
radial-gradient(circle at 80% 70%, rgba(140, 120, 255, 0.6), transparent 60%),
linear-gradient(135deg, #121428, #2a2f5e);
}
.media::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(160deg, rgba(10, 12, 20, 0.15), rgba(10, 12, 20, 0.55));
opacity: 0.7;
}
.content {
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
gap: 16px;
}
.title {
font-family: "Cardo", "Times New Roman", serif;
font-size: clamp(1.8rem, 3vw, 2.7rem);
margin: 0;
}
.copy {
margin: 0;
font-size: 1.05rem;
line-height: 1.65;
color: var(--muted);
}
.btn {
align-self: flex-start;
cursor: pointer;
padding: 0.75rem 1.5rem;
border-radius: 999px;
border: 1px solid rgba(115, 208, 255, 0.7);
background: rgba(115, 208, 255, 0.12);
color: var(--fg);
text-transform: uppercase;
letter-spacing: 0.15em;
font-size: 0.7rem;
transition: transform 0.2s var(--ease), background 0.2s var(--ease);
}
.btn:hover {
transform: translateY(-2px);
background: rgba(115, 208, 255, 0.28);
}
.meta {
font-size: 0.85rem;
letter-spacing: 0.3em;
text-transform: uppercase;
color: rgba(245, 244, 242, 0.7);
}
.footer {
padding: 64px clamp(24px, 5vw, 96px) 96px;
color: var(--muted);
}
@media (max-width: 900px) {
.card {
grid-template-columns: 1fr;
}
}
@media (prefers-reduced-motion: reduce) {
.card,
.media,
.btn {
transition: none;
}
}const cardsData = [
{
title: "Mountain View",
copy: "Check out these high-altitude escapes with crisp air and cinematic horizons.",
button: "View Trips",
theme: "media-sky",
},
{
title: "To The Beach",
copy: "Plan your next shoreline getaway with warm sunsets and deep blue water.",
button: "View Trips",
theme: "media-tide",
},
{
title: "Desert Destinations",
copy: "Wind-shaped dunes and amber tones for your most surreal journey yet.",
button: "Book Now",
theme: "media-dune",
},
{
title: "Explore The Galaxy",
copy: "Lift off into a night-sky adventure with glowing stardust and nebula haze.",
button: "Book Now",
theme: "media-orbit",
},
];
const stack = document.getElementById("card-stack");
const stage = document.getElementById("scroll-stage");
stack.innerHTML = cardsData
.map((card, index) => {
return `
<article class="card" data-index="${index}" aria-hidden="true">
<div class="media ${card.theme}"></div>
<div class="content">
<p class="meta">0${index + 1}</p>
<h2 class="title">${card.title}</h2>
<p class="copy">${card.copy}</p>
<button class="btn" type="button">${card.button}</button>
</div>
</article>
`;
})
.join("");
stage.style.setProperty("--card-count", cardsData.length);
const cards = Array.from(document.querySelectorAll(".card"));
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
const cycleMs = prefersReducedMotion.matches ? 2800 : 3800;
const transitionMs = prefersReducedMotion.matches ? 500 : 700;
function applyProgress(scaled) {
cards.forEach((card, index) => {
const distance = scaled - index;
const absDistance = Math.abs(distance);
const clamped = clamp(absDistance, 0, 1);
const opacity = 1 - clamped;
const slideX = distance * 110;
const scale = 1 - clamped * 0.06;
const blur = clamped * 12;
const wipe = clamp(1 - clamped, 0, 1);
card.style.opacity = opacity.toFixed(3);
card.style.transform = `translateX(${slideX.toFixed(2)}px) scale(${scale.toFixed(3)})`;
card.style.filter = `blur(${blur.toFixed(2)}px)`;
card.style.clipPath = `polygon(0 0, ${wipe * 100}% 0, ${wipe * 100}% 100%, 0 100%)`;
card.style.zIndex = String(cards.length - Math.round(absDistance * 10));
card.setAttribute("aria-hidden", opacity < 0.6 ? "true" : "false");
const media = card.querySelector(".media");
if (media) {
const mediaShift = clamp(-distance * 24, -30, 30);
media.style.transform = `translateX(${mediaShift.toFixed(2)}px) scale(${(1 + clamped * 0.04).toFixed(3)})`;
}
});
}
let rafId = 0;
let startTime = 0;
function animate(now) {
if (!startTime) startTime = now;
const elapsed = now - startTime;
const segment = cycleMs + transitionMs;
const total = segment * cards.length;
const loopTime = elapsed % total;
const activeIndex = Math.floor(loopTime / segment);
const segmentTime = loopTime % segment;
const t = clamp(segmentTime / transitionMs, 0, 1);
const eased = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
const scaled = activeIndex + eased;
applyProgress(scaled);
rafId = window.requestAnimationFrame(animate);
}
applyProgress(0);
rafId = window.requestAnimationFrame(animate);
prefersReducedMotion.addEventListener("change", () => {
window.cancelAnimationFrame(rafId);
startTime = 0;
rafId = window.requestAnimationFrame(animate);
});<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Card Automatic Transition</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Cardo:ital@0;1&family=Rubik:wght@400;600;700&display=swap"
rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="page">
<header class="hero">
<p class="kicker">Atlas Collection</p>
<h1>Scroll To Morph The Cards</h1>
<p class="lead">Each scroll segment slides, blurs, and reveals the next scene with a diagonal wipe.</p>
</header>
<section class="scroll-stage" id="scroll-stage">
<div class="card-stack" id="card-stack"></div>
</section>
<footer class="footer">
<p>Keep scrolling to replay the transition sequence.</p>
</footer>
</main>
<script src="https://cdn.jsdelivr.net/npm/@studio-freight/lenis@1.0.42/bundled/lenis.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<script src="script.js"></script>
</body>
</html>Card Automatic Transition
An animated card stack component that automatically cycles through cards with smooth transitions. Each card slides, blurs, and reveals the next scene with a diagonal wipe effect, creating a cinematic scrolling experience.
How it works
The component uses requestAnimationFrame to create a continuous animation loop that cycles through cards automatically. Each card’s visual state is calculated based on its distance from the active card:
- Opacity — fades in/out based on proximity
- Transform — slides horizontally and scales for depth
- Blur — applies blur filter for focus effect
- Clip-path — creates diagonal wipe reveal animation
- Z-index — manages stacking order dynamically
The animation respects prefers-reduced-motion and adjusts timing accordingly.
Key features
- Automatic card cycling with smooth transitions
- Scroll-triggered animations with diagonal wipe reveals
- Blur and scale effects for depth perception
- Responsive design with mobile-friendly layouts
- Accessibility support with
prefers-reduced-motion - Customizable card data and themes
When to use it
- Product showcases with multiple items
- Feature highlights in landing pages
- Portfolio galleries with smooth transitions
- Storytelling sections with sequential content
- Hero sections with animated card stacks