Word-by-word highlight
A pinned paragraph lights up one word at a time as you scroll through it.
Headings arrive as random characters and decode into words from left to right.
When a heading enters the viewport, a requestAnimationFrame loop rebuilds its text every frame: characters before a moving cursor are final, the rest are random symbols. The cursor advances with time, so longer headings take a little longer. Leaving the viewport resets them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scramble decode</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: #050a05; color: #39ff88; }
.list { padding: 40vh 8vw; display: grid; gap: 40vh; }
.list h2 { font: 700 clamp(1.8rem, 6vw, 4rem)/1.1 ui-monospace, Menlo, monospace; margin: 0; text-shadow: 0 0 18px rgba(57,255,136,.45); }
</style>
</head>
<body>
<main class="list">
<h2 data-text="Decrypting the page">Decrypting the page</h2>
<h2 data-text="One glyph at a time">One glyph at a time</h2>
<h2 data-text="Signal over noise">Signal over noise</h2>
<h2 data-text="Access granted">Access granted</h2>
</main>
<script>
const GLYPHS = '!<>-_/[]{}=+*^?#ABCDEFXYZ0123456789';
function decode(el) {
const target = el.dataset.text;
const start = performance.now();
cancelAnimationFrame(el._raf);
const tick = now => {
const cursor = Math.floor((now - start) / 45);
let out = '';
for (let i = 0; i < target.length; i++) {
if (i < cursor || target[i] === ' ') out += target[i];
else out += GLYPHS[Math.floor(Math.random() * GLYPHS.length)];
}
el.textContent = out;
if (cursor < target.length) el._raf = requestAnimationFrame(tick);
};
el._raf = requestAnimationFrame(tick);
}
const io = new IntersectionObserver(entries => entries.forEach(e => {
if (e.isIntersecting) decode(e.target);
else { cancelAnimationFrame(e.target._raf); e.target.textContent = e.target.dataset.text.replace(/\S/g, '#'); }
}), { threshold: 0.6 });
document.querySelectorAll('[data-text]').forEach(el => io.observe(el));
</script>
</body>
</html>