Window parallax

Images inside fixed frames slide more slowly than the frames themselves.

How it works

Each frame clips a picture that is 40% taller than it. The script maps the frame’s position in the viewport to a translateY of the picture from −20% to +20%, so the picture lags behind the frame.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Window 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: #f6f4f0; color: #222; }
.story { max-width: 720px; margin: 0 auto; padding: 20vh 24px 40vh; font: 1.15rem/1.65 Georgia, serif; }
.story h1 { font: 800 3rem/1 system-ui, sans-serif; letter-spacing: -.03em; }
.win { height: 55vh; overflow: hidden; border-radius: 6px; margin: 8vh 0; }
.pic { height: 140%; margin-top: -20%; will-change: transform; }
.p1 { background: radial-gradient(circle at 70% 30%, #fff8d6 0 8%, transparent 9%), linear-gradient(#78c1ff, #c8e7ff 55%, #3f8f5a 56%, #2b6b42); }
.p2 { background: repeating-linear-gradient(90deg, #f2b5d4 0 40px, #7bdff2 40px 80px, #b2f7ef 80px 120px); }
.p3 { background: radial-gradient(circle at 30% 70%, #ffd166, transparent 30%), radial-gradient(circle at 75% 35%, #ef476f, transparent 35%), #073b4c; }
</style>
</head>
<body>
<article class="story">
  <h1>Window parallax</h1>
  <p>Frames scroll at full speed. What is inside them lags behind.</p>
  <div class="win"><div class="pic p1"></div></div>
  <p>This is the most common parallax on the web: one image, one frame, one transform. It works on any layout without changing the flow of the page.</p>
  <div class="win"><div class="pic p2"></div></div>
  <p>Use it for photos in long-form articles, where a little movement makes a static image feel placed in space.</p>
  <div class="win"><div class="pic p3"></div></div>
</article>
<script>
const wins = document.querySelectorAll('.win');
function update() {
  const vh = innerHeight;
  wins.forEach(w => {
    const r = w.getBoundingClientRect();
    const p = (r.top + r.height / 2 - vh / 2) / (vh / 2 + r.height / 2);
    w.firstElementChild.style.transform = 'translate3d(0,' + (p * 20) + '%,0)';
  });
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>

More parallax effects