Cards & galleries
Flip card
Pure CSSHover, tap or focus to flip. The back face is hidden until it turns toward you.
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);
}
&__nav {
position: absolute;
bottom: 10px;
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);
}
}
}
}