Word-by-word highlight
A pinned paragraph lights up one word at a time as you scroll through it.
A sentence types itself out, character by character, exactly as fast as you scroll.
The full sentence is stored once. Progress through a tall section maps to a number of characters, and the visible text is a slice of the sentence up to that number, with a blinking caret at the end.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scroll typewriter</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: #0d1117; color: #7ee787; }
.track { height: 320vh; }
.screen { position: sticky; top: 0; height: 100vh; display: grid; place-items: center; padding: 0 8vw; }
.type { font: 500 clamp(1.4rem, 3.4vw, 2.6rem)/1.4 ui-monospace, Menlo, monospace; max-width: 32ch; margin: 0; }
.caret { display: inline-block; width: .6em; height: 1.1em; background: currentColor; vertical-align: -.15em; margin-left: 2px; animation: blink 1s steps(1) infinite; }
@keyframes blink { 50% { opacity: 0; } }
</style>
</head>
<body>
<section class="track"><div class="screen"><p class="type"><span id="out"></span><i class="caret"></i></p></div></section>
<script>
const text = '$ echo "Every keystroke here is a scroll event." Scroll faster and I type faster. Stop, and so do I. Scroll back up to delete.';
const out = document.getElementById('out');
const track = document.querySelector('.track');
function update() {
const r = track.getBoundingClientRect();
const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
out.textContent = text.slice(0, Math.round(p * text.length));
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>