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.
JS gives each cover its offset from the active one; CSS calc() does the placement and easing.
Your edited version
--o (offset from active), --abs and --sign.calc() — JS never writes a transform.transition animates every move.z-index by distance keeps nearer covers on top.calc() with per-item custom propertiestransitionkeyboard arrows<div class="coverflow" tabindex="0">
<div class="track">
<i style="--hue:250">1</i><i style="--hue:272">2</i>
<i style="--hue:294">3</i><i style="--hue:316">4</i>
<i style="--hue:338">5</i><i style="--hue:360">6</i>
<i style="--hue:22">7</i>
</div>
<nav>
<button type="button" data-dir="-1" aria-label="Previous">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m15 18-6-6 6-6"/></svg>
</button>
<button type="button" data-dir="1" aria-label="Next">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 18 6-6-6-6"/></svg>
</button>
</nav>
</div>
.coverflow {
display: grid;
justify-items: center;
gap: 40px;
perspective: 700px;
outline: none;
}
.track {
position: relative;
width: 130px;
height: 165px;
transform-style: preserve-3d;
pointer-events: none; /* same plane as the active cover: let the covers take the pointer */
}
.track i {
/* --o, --abs and --sign are set per cover from JS */
position: absolute;
inset: 0;
display: grid;
place-items: center;
border-radius: 12px;
font: 900 2.2rem system-ui;
color: #fff;
cursor: pointer;
pointer-events: auto;
background: linear-gradient(160deg, hsl(var(--hue) 85% 64%), hsl(var(--hue) 70% 36%));
box-shadow: 0 16px 24px -14px #000;
transform:
translateX(calc(var(--o) * 62px + var(--sign) * 46px))
translateZ(calc(var(--abs) * -60px))
rotateY(calc(var(--sign) * -58deg));
transition: transform 0.55s cubic-bezier(0.2, 0.8, 0.2, 1);
}
nav { display: flex; gap: 8px; }
nav button {
width: 44px;
height: 36px;
border: 1px solid #5a6188;
border-radius: 8px;
display: grid;
place-items: center;
padding: 0;
background: #161a2e;
color: #fff;
cursor: pointer;
}
/* SVG arrows, not text like ‹ ›: a glyph sits on the font's baseline, so it never centres */
nav svg {
width: 18px;
height: 18px;
}
const root = document.querySelector('.coverflow');
const items = [...root.querySelectorAll('.track i')];
let active = Math.floor(items.length / 2);
function layout() {
items.forEach((el, i) => {
const o = i - active;
el.style.setProperty('--o', o);
el.style.setProperty('--abs', Math.abs(o));
el.style.setProperty('--sign', Math.sign(o));
el.style.zIndex = items.length - Math.abs(o);
});
}
function go(dir) {
active = Math.max(0, Math.min(items.length - 1, active + dir));
layout();
}
root.addEventListener('click', (e) => {
const btn = e.target.closest('[data-dir]');
if (btn) return go(Number(btn.dataset.dir));
const i = items.indexOf(e.target);
if (i >= 0) go(i - active);
});
root.addEventListener('keydown', (e) => {
if (e.key === 'ArrowLeft') go(-1);
if (e.key === 'ArrowRight') go(1);
});
layout();
.d-coverflow {
display: grid;
place-items: center;
width: 100%;
height: 100%;
perspective: 700px;
&__track {
position: relative;
width: 96px;
height: 120px;
margin-bottom: 26px;
transform-style: preserve-3d;
pointer-events: none; // coplanar with the active cover: let the covers take the pointer
}
i {
// JS sets, per cover: --o (offset from active), --abs (|offset|), --sign (-1 / 0 / 1)
position: absolute;
inset: 0;
display: grid;
place-items: center;
border-radius: 12px;
background: linear-gradient(160deg, hsl(var(--hue) 85% 64%), hsl(var(--hue) 70% 36%));
color: #fff;
font-size: 30px;
font-style: normal;
font-weight: 900;
cursor: pointer;
pointer-events: auto;
box-shadow: 0 16px 24px -14px #000;
transform:
translateX(calc(var(--o) * 46px + var(--sign) * 34px))
translateZ(calc(var(--abs) * -50px))
rotateY(calc(var(--sign) * -58deg));
transition: transform 0.55s cubic-bezier(0.2, 0.8, 0.2, 1);
}
// the control dock: the same place as in every demo with controls
&__nav {
position: absolute;
bottom: 14px;
display: flex;
gap: 8px;
button {
width: 34px;
height: 28px;
border: 1px solid var(--border-strong);
border-radius: 8px;
background: var(--surface);
display: grid;
place-items: center;
padding: 0;
cursor: pointer;
&:hover {
border-color: var(--warm);
}
}
}
}