Inertia lag parallax
Columns chase the scroll position with different amounts of smoothing, so they trail behind and catch up like they have weight.
JavaScriptAdvanced
<div class="space"><p>Scroll quickly and stop:<br>the columns catch up one by one</p></div>
<div class="grid">
<div class="col" data-ease="0.18"><small>fast</small><div class="tile">1</div><div class="tile">2</div><div class="tile">3</div><div class="tile">4</div></div>
<div class="col" data-ease="0.09"><small>medium</small><div class="tile">5</div><div class="tile">6</div><div class="tile">7</div><div class="tile">8</div></div>
<div class="col" data-ease="0.045"><small>slow</small><div class="tile">9</div><div class="tile">10</div><div class="tile">11</div><div class="tile">12</div></div>
</div>
body{display:block;height:auto}
.space{height:40vh;display:grid;place-items:center;color:#a6938f;font-size:14px;text-align:center}
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;width:min(520px,92vw);margin:0 auto;padding-bottom:160px}
.col{display:grid;gap:12px;align-content:start;will-change:transform}
.tile{height:170px;border-radius:12px;display:grid;place-items:center;font-weight:800;color:#270100}
.col:nth-child(1) .tile{background:#f08a5d}.col:nth-child(2) .tile{background:#f3d9c8}.col:nth-child(3) .tile{background:#e8b04c}
.col small{display:block;text-align:center;color:#a6938f;font-size:12px;margin-bottom:6px}
const cols = [...document.querySelectorAll('.col')].map(el => ({ el, ease: +el.dataset.ease, cur: scrollY }));
(function loop() {
const target = scrollY;
cols.forEach(c => {
c.cur += (target - c.cur) * c.ease; // chase the real scroll position
// The column naturally moves with the page. Adding (scrollY - cur) makes it
// appear to be at the lagging position instead.
c.el.style.transform = `translate3d(0, ${target - c.cur}px, 0)`;
});
requestAnimationFrame(loop);
})();
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Inertia lag 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:40vh;display:grid;place-items:center;color:#a6938f;font-size:14px;text-align:center}
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;width:min(520px,92vw);margin:0 auto;padding-bottom:160px}
.col{display:grid;gap:12px;align-content:start;will-change:transform}
.tile{height:170px;border-radius:12px;display:grid;place-items:center;font-weight:800;color:#270100}
.col:nth-child(1) .tile{background:#f08a5d}.col:nth-child(2) .tile{background:#f3d9c8}.col:nth-child(3) .tile{background:#e8b04c}
.col small{display:block;text-align:center;color:#a6938f;font-size:12px;margin-bottom:6px}
</style></head>
<body>
<div class="space"><p>Scroll quickly and stop:<br>the columns catch up one by one</p></div>
<div class="grid">
<div class="col" data-ease="0.18"><small>fast</small><div class="tile">1</div><div class="tile">2</div><div class="tile">3</div><div class="tile">4</div></div>
<div class="col" data-ease="0.09"><small>medium</small><div class="tile">5</div><div class="tile">6</div><div class="tile">7</div><div class="tile">8</div></div>
<div class="col" data-ease="0.045"><small>slow</small><div class="tile">9</div><div class="tile">10</div><div class="tile">11</div><div class="tile">12</div></div>
</div>
<script>
const cols = [...document.querySelectorAll('.col')].map(el => ({ el, ease: +el.dataset.ease, cur: scrollY }));
(function loop() {
const target = scrollY;
cols.forEach(c => {
c.cur += (target - c.cur) * c.ease; // chase the real scroll position
// The column naturally moves with the page. Adding (scrollY - cur) makes it
// appear to be at the lagging position instead.
c.el.style.transform = `translate3d(0, ${target - c.cur}px, 0)`;
});
requestAnimationFrame(loop);
})();
</script>
</body></html>
Good for
Premium portfolio galleries and editorial pages that want a floaty, luxurious scroll.
Watch out for
lerp (current += (target - current) * ease) must run every frame. Stop the loop when the gap is under a pixel to save battery.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.