Hover card fan
Pure CSSThree overlapping cards: the one you point at straightens and comes toward you while the others dim and lean away. Static strips catch the pointer, so nothing flickers.
Click the deck: the top card flies off and returns at the back. JS only reassigns positions.
Your edited version
--p (0 = top).z-index via calc()..is-leaving to the top card (it flies off), waits for the transition, then moves that card to the end of the order and rewrites --p for all.--p changed.position index in --pcalc() for offset, depth and z-indexclass toggle for the exit<div class="scene">
<div class="stack">
<i style="--hue:255">One</i>
<i style="--hue:290">Two</i>
<i style="--hue:325">Three</i>
<i style="--hue:360">Four</i>
</div>
</div>
.scene {
perspective: 800px;
}
.stack {
position: relative;
width: 230px;
height: 150px;
cursor: pointer;
transform-style: preserve-3d;
transform: rotateX(18deg) rotateY(-14deg);
}
.stack i {
/* --p = position in the deck, set from JS */
position: absolute;
inset: 0;
z-index: calc(10 - var(--p));
display: grid;
place-items: center;
border-radius: 16px;
color: #fff;
font: 900 1.6rem system-ui;
background: linear-gradient(135deg, hsl(var(--hue) 85% 64%), hsl(calc(var(--hue) + 40) 80% 46%));
box-shadow: 0 12px 22px -12px #000;
opacity: calc(1 - var(--p) * 0.18);
transform: translateY(calc(var(--p) * -14px)) translateZ(calc(var(--p) * -44px));
transition: transform 0.38s cubic-bezier(0.3, 1.2, 0.5, 1), opacity 0.38s;
}
.stack i.is-leaving {
opacity: 0;
transform: translateX(130%) translateZ(60px) rotateY(-35deg) rotateZ(18deg);
}
const stack = document.querySelector('.stack');
let order = [...stack.querySelectorAll('i')];
let busy = false;
function layout() {
order.forEach((card, p) => card.style.setProperty('--p', p));
}
stack.addEventListener('click', () => {
if (busy) return;
busy = true;
const top = order[0];
top.classList.add('is-leaving');
setTimeout(() => {
order = [...order.slice(1), top]; // top card goes to the back
top.classList.remove('is-leaving');
layout();
busy = false;
}, 380); // same as the CSS transition
});
layout();
.d-cardstack {
position: relative;
width: 170px;
height: 110px;
cursor: pointer;
transform-style: preserve-3d;
transform: rotateX(18deg) rotateY(-14deg);
outline-offset: 30px;
i {
// --p is the card's position in the deck: 0 = top. JS reassigns it, CSS does the rest.
position: absolute;
inset: 0;
z-index: calc(10 - var(--p));
display: grid;
place-items: center;
border-radius: 14px;
background: linear-gradient(135deg, hsl(var(--hue) 85% 64%), hsl(calc(var(--hue) + 40) 80% 46%));
color: #fff;
font-size: 22px;
font-style: normal;
font-weight: 900;
box-shadow: 0 12px 22px -12px #000;
opacity: calc(1 - var(--p) * 0.18);
transform: translateY(calc(var(--p) * -12px)) translateZ(calc(var(--p) * -38px));
transition:
transform 0.38s cubic-bezier(0.3, 1.2, 0.5, 1),
opacity 0.38s;
&.is-leaving {
opacity: 0;
transform: translateX(130%) translateZ(60px) rotateY(-35deg) rotateZ(18deg);
}
}
}