Tilt-in cards
Cards lie back like pages on a desk and stand upright as they reach the middle of the screen.
A pinned 3D cube turns to a new face for each quarter of the section.
Six faces are positioned with rotate and translateZ inside a preserve-3d container. Scroll progress through a 400vh section maps to rotateY from 0 to 270 degrees, with a little rotateX for perspective.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scroll cube</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; }
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; }
body { background: radial-gradient(circle at 50% 40%, #2b2d42, #0e0f1a); color: #fff; }
.track { height: 400vh; }
.stage { position: sticky; top: 0; height: 100vh; display: grid; place-items: center; perspective: 1000px; }
.cube { --s: 40vmin; width: var(--s); height: var(--s); position: relative; transform-style: preserve-3d; }
.face {
position: absolute; inset: 0; display: grid; place-items: center;
font: 800 clamp(1.5rem, 5vmin, 3rem) system-ui, sans-serif; border: 2px solid rgba(255,255,255,.4);
backface-visibility: hidden;
}
.f1 { background: #ef233c; transform: translateZ(calc(var(--s) / 2)); }
.f2 { background: #f77f00; transform: rotateY(90deg) translateZ(calc(var(--s) / 2)); }
.f3 { background: #3a86ff; transform: rotateY(180deg) translateZ(calc(var(--s) / 2)); }
.f4 { background: #06d6a0; transform: rotateY(-90deg) translateZ(calc(var(--s) / 2)); }
.f5 { background: #8d99ae; transform: rotateX(90deg) translateZ(calc(var(--s) / 2)); }
.f6 { background: #2b2d42; transform: rotateX(-90deg) translateZ(calc(var(--s) / 2)); }
</style>
</head>
<body>
<section class="track">
<div class="stage">
<div class="cube" id="cube">
<div class="face f1">Front</div><div class="face f2">Right</div>
<div class="face f3">Back</div><div class="face f4">Left</div>
<div class="face f5"></div><div class="face f6"></div>
</div>
</div>
</section>
<script>
const cube = document.getElementById('cube');
const track = document.querySelector('.track');
function update() {
const r = track.getBoundingClientRect();
const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
cube.style.transform = 'rotateX(' + (-18 + p * 10) + 'deg) rotateY(' + (-p * 270) + 'deg)';
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>