Filmstrip parallax

A pinned filmstrip slides sideways while the picture inside every frame drifts the other way.

How it works

Vertical scroll is mapped to translateX on the strip, as in a horizontal pin. In the same frame, each picture is offset by its frame’s distance from the screen centre, so the contents lag behind the frames and gain depth.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Filmstrip parallax</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: #111; color: #eee; }
.sticky { position: sticky; top: 0; height: 100vh; overflow: hidden; display: flex; align-items: center; }
.strip { display: flex; gap: 22px; padding: 26px 10vw; background: #000; will-change: transform;
  background-image: radial-gradient(circle, #222 0 5px, transparent 6px), radial-gradient(circle, #222 0 5px, transparent 6px);
  background-size: 28px 13px; background-repeat: repeat-x; background-position: 0 4px, 0 calc(100% - 4px); }
.frame { flex: 0 0 auto; width: min(60vw, 520px); aspect-ratio: 4 / 3; overflow: hidden; border-radius: 4px; position: relative; }
.frame div { position: absolute; top: 0; bottom: 0; left: -20%; right: -20%; will-change: transform; }
.frame span { position: absolute; left: 14px; bottom: 10px; font: 700 .9rem ui-monospace, monospace; color: #fff; text-shadow: 0 1px 4px #000; }
.end { height: 60vh; display: grid; place-content: center; }
</style>
</head>
<body>
<section class="pin" id="pin">
  <div class="sticky">
    <div class="strip" id="strip"></div>
  </div>
</section>
<div class="end">That was the reel.</div>
<script>
const scenes = [
  'radial-gradient(circle at 30% 40%, #ffd166 0 10%, transparent 11%), linear-gradient(#ef476f, #ffd166)',
  'repeating-linear-gradient(90deg, #06d6a0 0 40px, #118ab2 40px 80px)',
  'radial-gradient(circle at 60% 60%, #fff 0 4%, transparent 5%), radial-gradient(circle at 20% 30%, #fff 0 2%, transparent 3%), #073b4c',
  'linear-gradient(135deg, #f72585 25%, #7209b7 25% 50%, #3a0ca3 50% 75%, #4361ee 75%)',
  'radial-gradient(ellipse at 50% 100%, #2a9d8f 0 40%, transparent 41%), linear-gradient(#e9c46a, #f4a261)',
  'conic-gradient(from 45deg at 50% 50%, #ff595e, #ffca3a, #8ac926, #1982c4, #6a4c93, #ff595e)',
  'radial-gradient(circle at 70% 30%, #ffbe0b 0 12%, transparent 13%), linear-gradient(#3a86ff, #8338ec)'
];
const strip = document.getElementById('strip');
scenes.forEach((bg, i) => strip.insertAdjacentHTML('beforeend', '<div class="frame"><div style="background:' + bg + '"></div><span>' + String(i + 1).padStart(2, '0') + 'A</span></div>'));
const frameEls = [...strip.querySelectorAll('.frame')];
const pin = document.getElementById('pin');
function size() { pin.style.height = (strip.scrollWidth - innerWidth + innerHeight) + 'px'; update(); }
function update() {
  const r = pin.getBoundingClientRect();
  const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
  strip.style.transform = 'translate3d(' + (-p * (strip.scrollWidth - innerWidth)) + 'px,0,0)';
  const mid = innerWidth / 2;
  frameEls.forEach(f => {
    const fr = f.getBoundingClientRect();
    const d = (fr.left + fr.width / 2 - mid) / innerWidth;
    f.firstChild.style.transform = 'translate3d(' + (-d * 18) + '%,0,0)';
  });
}
addEventListener('scroll', update, { passive: true });
addEventListener('resize', size);
size();
</script>
</body>
</html>

More horizontal effects