Velocity skew
Headlines lean into the direction of travel when you scroll fast and spring back when you stop.
A pinned paragraph lights up one word at a time as you scroll through it.
The paragraph is split into word spans and pinned with sticky. Scroll progress through a 300vh section maps to an index; words before it are full opacity, words after it are dim, and the current word blends between the two.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Word-by-word highlight</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: #0b0f14; color: #fff; }
.track { height: 300vh; }
.para {
position: sticky; top: 0; height: 100vh; margin: 0; padding: 0 8vw;
display: flex; flex-wrap: wrap; align-content: center; gap: .1em .3em;
font: 700 clamp(1.8rem, 4.6vw, 3.6rem)/1.15 system-ui, sans-serif; letter-spacing: -.02em;
}
.para span { opacity: .15; }
.tail { height: 60vh; display: grid; place-content: center; opacity: .5; }
</style>
</head>
<body>
<section class="track">
<p class="para" id="para">Good interfaces do not shout. They wait for you to move, and then they answer. A page that reveals itself as you read feels less like a document and more like a conversation, one word at a time.</p>
</section>
<div class="tail">The end.</div>
<script>
const para = document.getElementById('para');
const words = para.textContent.trim().split(/\s+/);
para.innerHTML = words.map(w => '<span>' + w + '</span>').join('');
const spans = para.querySelectorAll('span');
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 pos = p * spans.length;
spans.forEach((s, i) => {
s.style.opacity = (0.15 + 0.85 * Math.min(1, Math.max(0, pos - i))).toFixed(2);
});
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>