Layered landscape
Five mountain ridges slide at different speeds to build depth out of flat shapes.
Three columns of tiles glide in opposite directions while the page holds still.
A pinned stage holds three tall columns. Scroll progress moves the outer columns up and the middle column down by the same amount, so the grid seems to shear past itself.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Split columns</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: #faf3dd; color: #2f3e46; }
.track { height: 300vh; }
.stage { position: sticky; top: 0; height: 100vh; overflow: hidden; }
.cols { display: grid; grid-template-columns: repeat(3, 1fr); gap: 2vw; padding: 0 3vw; }
.col { display: grid; gap: 2vw; will-change: transform; }
.col i { aspect-ratio: .8; border-radius: 14px; background: var(--c); }
</style>
</head>
<body>
<section class="track">
<div class="stage"><div class="cols" id="cols"></div></div>
</section>
<script>
const colors = ['#e07a5f', '#3d405b', '#81b29a', '#f2cc8f', '#c1121f', '#669bbc', '#ffb4a2', '#6d6875'];
const cols = document.getElementById('cols');
const els = [0, 1, 2].map(k => {
const c = document.createElement('div');
c.className = 'col';
for (let i = 0; i < 9; i++) c.insertAdjacentHTML('beforeend', '<i style="--c:' + colors[(i * 3 + k * 5) % colors.length] + '"></i>');
cols.appendChild(c);
return c;
});
const track = document.querySelector('.track');
function update() {
const r = track.getBoundingClientRect();
const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
const range = els[0].offsetHeight - innerHeight;
els.forEach((c, k) => {
const y = k === 1 ? -range + p * range : -p * range;
c.style.transform = 'translate3d(0,' + y + 'px,0)';
});
}
addEventListener('scroll', update, { passive: true });
addEventListener('resize', update);
update();
</script>
</body>
</html>