Letter assemble

Scattered, spinning letters fly together into a word as you scroll, and scatter again on the way back.

How it works

Each letter gets a random start offset and rotation. Scroll progress through a pinned section interpolates every letter from that random state to zero, with a small per-letter delay so they land one after another.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Letter assemble</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: #ffe5ec; color: #3d0066; overflow-x: hidden; }
.track { height: 260vh; }
.stage { position: sticky; top: 0; height: 100vh; display: grid; place-items: center; overflow: hidden; }
.word { display: flex; margin: 0; font: 900 clamp(2.6rem, 12vw, 9rem)/1 system-ui, sans-serif; letter-spacing: -.02em; }
.word span { display: inline-block; will-change: transform; }
.tail { height: 60vh; display: grid; place-content: center; font-size: 1.4rem; }
</style>
</head>
<body>
<section class="track"><div class="stage"><h1 class="word" id="word">ASSEMBLE</h1></div></section>
<div class="tail">Assembled.</div>
<script>
const word = document.getElementById('word');
const letters = [...word.textContent].map(ch => {
  const s = document.createElement('span');
  s.textContent = ch;
  return { el: s, x: (Math.random() - 0.5) * 120, y: (Math.random() - 0.5) * 120, r: (Math.random() - 0.5) * 540 };
});
word.textContent = '';
letters.forEach(l => word.appendChild(l.el));
const track = document.querySelector('.track');
const ease = t => 1 - Math.pow(1 - t, 3);
function update() {
  const rect = track.getBoundingClientRect();
  const p = Math.min(1, Math.max(0, -rect.top / (rect.height - innerHeight)));
  letters.forEach((l, i) => {
    const k = 1 - ease(Math.min(1, Math.max(0, p * 1.5 - i * 0.05)));
    l.el.style.transform = 'translate(' + (l.x * k) + 'vw,' + (l.y * k) + 'vh) rotate(' + (l.r * k) + 'deg)';
    l.el.style.opacity = (1 - k * 0.7).toFixed(2);
  });
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>

More text effects