Zoom parallax
A pinned collage zooms toward you as you scroll. Each picture scales at its own rate, so the outer ones rush past the centre.
CSSJavaScriptAdvanced
<div class="space"><p>Scroll down<br>and fall into the picture</p></div>
<section class="track" id="track"><div class="pin">
<div class="layer" data-k="2.2"><div class="tile c"></div></div>
<div class="layer" data-k="3.2"><div class="tile a"></div></div>
<div class="layer" data-k="4"><div class="tile b"></div></div>
<div class="layer" data-k="5"><div class="tile d"></div></div>
<div class="layer" data-k="6"><div class="tile e"></div></div>
</div></section>
<div class="space"><p>You arrived.</p></div>
body{display:block;height:auto}
.space{height:50vh;display:grid;place-items:center;color:#a6938f;font-size:14px;text-align:center}
.track{height:300vh}
.pin{position:sticky;top:0;height:100vh;overflow:hidden}
.layer{position:absolute;inset:0;display:grid;place-items:center;will-change:transform,opacity}
.tile{position:absolute;border-radius:12px}
.c{width:28vmin;height:36vmin;background:linear-gradient(160deg,#e5603a,#8a3a22)}
.a{width:22vmin;height:16vmin;left:8%;top:12%;background:linear-gradient(160deg,#6e2a18,#f08a5d)}
.b{width:18vmin;height:24vmin;right:10%;top:16%;background:linear-gradient(160deg,#e8b04c,#d98a8a)}
.d{width:20vmin;height:14vmin;left:12%;bottom:14%;background:linear-gradient(160deg,#f3d9c8,#b07a6a)}
.e{width:26vmin;height:18vmin;right:8%;bottom:12%;background:linear-gradient(160deg,#f08a5d,#a8523a)}
const track = document.getElementById('track');
const layers = [...document.querySelectorAll('.layer')];
function update() {
const max = track.offsetHeight - innerHeight;
const p = Math.min(Math.max(-track.getBoundingClientRect().top / max, 0), 1);
layers.forEach(l => {
l.style.transform = `scale(${1 + p * l.dataset.k})`; // bigger multiplier = closer to camera
l.style.opacity = 1 - Math.max(p - 0.7, 0) / 0.3; // fade out at the very end
});
}
addEventListener('scroll', () => requestAnimationFrame(update), { passive: true });
update();
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Zoom parallax</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;height:100%}
body{display:grid;place-items:center;background:#140807;color:#fffffc;font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
body{display:block;place-items:normal;height:auto}
.space{height:50vh;display:grid;place-items:center;color:#a6938f;font-size:14px;text-align:center}
.track{height:300vh}
.pin{position:sticky;top:0;height:100vh;overflow:hidden}
.layer{position:absolute;inset:0;display:grid;place-items:center;will-change:transform,opacity}
.tile{position:absolute;border-radius:12px}
.c{width:28vmin;height:36vmin;background:linear-gradient(160deg,#e5603a,#8a3a22)}
.a{width:22vmin;height:16vmin;left:8%;top:12%;background:linear-gradient(160deg,#6e2a18,#f08a5d)}
.b{width:18vmin;height:24vmin;right:10%;top:16%;background:linear-gradient(160deg,#e8b04c,#d98a8a)}
.d{width:20vmin;height:14vmin;left:12%;bottom:14%;background:linear-gradient(160deg,#f3d9c8,#b07a6a)}
.e{width:26vmin;height:18vmin;right:8%;bottom:12%;background:linear-gradient(160deg,#f08a5d,#a8523a)}
</style></head>
<body>
<div class="space"><p>Scroll down<br>and fall into the picture</p></div>
<section class="track" id="track"><div class="pin">
<div class="layer" data-k="2.2"><div class="tile c"></div></div>
<div class="layer" data-k="3.2"><div class="tile a"></div></div>
<div class="layer" data-k="4"><div class="tile b"></div></div>
<div class="layer" data-k="5"><div class="tile d"></div></div>
<div class="layer" data-k="6"><div class="tile e"></div></div>
</div></section>
<div class="space"><p>You arrived.</p></div>
<script>
const track = document.getElementById('track');
const layers = [...document.querySelectorAll('.layer')];
function update() {
const max = track.offsetHeight - innerHeight;
const p = Math.min(Math.max(-track.getBoundingClientRect().top / max, 0), 1);
layers.forEach(l => {
l.style.transform = `scale(${1 + p * l.dataset.k})`; // bigger multiplier = closer to camera
l.style.opacity = 1 - Math.max(p - 0.7, 0) / 0.3; // fade out at the very end
});
}
addEventListener('scroll', () => requestAnimationFrame(update), { passive: true });
update();
</script>
</body></html>
Good for
Dramatic portfolio and product intros that hand over to the page content.
Watch out for
Scale from each layer's centre, give outer tiles a larger multiplier, and fade out at the end so nothing hits the camera abruptly.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.