Scroll text fill
A paragraph lights up word by word as you scroll, from dim grey to full white.
JavaScriptCSSIntermediate
<div class="space">Scroll down</div>
<div class="wrap" id="wrap"><div class="pin">
<p class="statement" id="p" aria-label="We make motion feel natural and easy to reuse.">We make motion feel natural and easy to reuse.</p>
</div></div>
<div class="space">The end</div>
body{display:block;height:auto}
.space{height:50vh;display:grid;place-items:center;color:#a6938f;font-size:14px}
.wrap{height:200vh}
.pin{position:sticky;top:0;height:100vh;display:grid;place-items:center;padding:0 7vw}
p.statement{margin:0;max-width:20ch;font-size:clamp(1.7rem,7vw,3.2rem);font-weight:700;letter-spacing:-.02em;line-height:1.15}
.w{color:#3a2724;transition:color .25s}
.w.on{color:#fff}
const p = document.getElementById('p');
const words = p.textContent.split(' ');
p.setAttribute('aria-label', p.textContent);
p.innerHTML = words.map(w => `<span class="w" aria-hidden="true">${w}</span>`).join(' ');
const spans = [...p.querySelectorAll('.w')];
const wrap = document.getElementById('wrap');
function update() {
const max = wrap.offsetHeight - innerHeight;
const prog = Math.min(Math.max(-wrap.getBoundingClientRect().top / max, 0), 1);
const lit = Math.round(prog * spans.length);
spans.forEach((s, i) => s.classList.toggle('on', i < lit));
}
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>Scroll text fill</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}
.wrap{height:200vh}
.pin{position:sticky;top:0;height:100vh;display:grid;place-items:center;padding:0 7vw}
p.statement{margin:0;max-width:20ch;font-size:clamp(1.7rem,7vw,3.2rem);font-weight:700;letter-spacing:-.02em;line-height:1.15}
.w{color:#3a2724;transition:color .25s}
.w.on{color:#fff}
</style></head>
<body>
<div class="space">Scroll down</div>
<div class="wrap" id="wrap"><div class="pin">
<p class="statement" id="p" aria-label="We make motion feel natural and easy to reuse.">We make motion feel natural and easy to reuse.</p>
</div></div>
<div class="space">The end</div>
<script>
const p = document.getElementById('p');
const words = p.textContent.split(' ');
p.setAttribute('aria-label', p.textContent);
p.innerHTML = words.map(w => `<span class="w" aria-hidden="true">${w}</span>`).join(' ');
const spans = [...p.querySelectorAll('.w')];
const wrap = document.getElementById('wrap');
function update() {
const max = wrap.offsetHeight - innerHeight;
const prog = Math.min(Math.max(-wrap.getBoundingClientRect().top / max, 0), 1);
const lit = Math.round(prog * spans.length);
spans.forEach((s, i) => s.classList.toggle('on', i < lit));
}
addEventListener('scroll', () => requestAnimationFrame(update), { passive: true });
update();
</script>
</body></html>
Good for
Statement sections and mission text where you want the reader to slow down.
Watch out for
Split text with JS so the HTML stays a normal readable paragraph, and keep aria-label on the original.
Accessibility
Wrap motion in @media (prefers-reduced-motion: reduce) and switch it off or shorten it for people who ask for less movement.