Cylinder carousel

Cards arranged around a 3D cylinder spin past the viewer as you scroll.

How it works

Ten cards are placed with rotateY(i × 36°) translateZ(radius) inside a preserve-3d ring, so they form a cylinder. Scroll progress rotates the ring a full turn. The radius is computed from the card width so neighbours just touch.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cylinder carousel</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% 30%, #283c63, #0b1021); color: #fff; }
.track { height: 400vh; }
.stage { position: sticky; top: 0; height: 100vh; display: grid; place-items: center; perspective: 1100px; overflow: hidden; }
.ring { position: relative; width: var(--w); height: calc(var(--w) * 1.3); transform-style: preserve-3d; --w: min(34vw, 220px); }
.ring article { position: absolute; inset: 0; border-radius: 14px; display: grid; place-items: end start; padding: 16px;
  font: 800 clamp(1rem, 2.6vw, 1.5rem) system-ui, sans-serif; backface-visibility: hidden; box-shadow: 0 0 0 1px rgba(255,255,255,.15) inset; }
</style>
</head>
<body>
<section class="track">
  <div class="stage"><div class="ring" id="ring"></div></div>
</section>
<script>
const ring = document.getElementById('ring');
const N = 10;
const hues = [0, 30, 55, 120, 170, 200, 230, 265, 300, 330];
for (let i = 0; i < N; i++) ring.insertAdjacentHTML('beforeend', '<article style="background:linear-gradient(160deg,hsl(' + hues[i] + ',85%,62%),hsl(' + (hues[i] + 40) + ',75%,40%))">No. ' + (i + 1) + '</article>');
const cards = ring.querySelectorAll('article');
function layout() {
  const w = ring.offsetWidth;
  const radius = Math.round((w / 2) / Math.tan(Math.PI / N)) + 16;
  cards.forEach((c, i) => { c.style.transform = 'rotateY(' + (i * 360 / N) + 'deg) translateZ(' + radius + 'px)'; });
  ring.dataset.r = radius;
  update();
}
const track = document.querySelector('.track');
function update() {
  const r = track.getBoundingClientRect();
  const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
  ring.style.transform = 'translateZ(' + (-ring.dataset.r) + 'px) rotateX(-6deg) rotateY(' + (-p * 360) + 'deg)';
}
addEventListener('scroll', update, { passive: true });
addEventListener('resize', layout);
layout();
</script>
</body>
</html>

More 3d effects