Split-flap board

An airport departures board that rattles through letters to a new word at each step of the scroll.

How it works

Scroll progress picks a word from a list. When the word changes, every cell cycles through a few random characters before landing on its letter, with a small delay per cell. Each change restarts a short rotateX animation on the cell, which reads as a flap dropping.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Split-flap board</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: #101114; color: #f7f3e8; }
.track { height: 500vh; }
.stage { position: sticky; top: 0; height: 100vh; display: grid; place-content: center; justify-items: center; gap: 24px; padding: 0 4vw; }
.label { margin: 0; font: 600 .85rem ui-monospace, monospace; letter-spacing: .3em; color: #8d8a80; text-transform: uppercase; }
.board { display: flex; gap: 5px; perspective: 400px; }
.cell {
  position: relative; width: clamp(28px, 8vw, 78px); aspect-ratio: .72; border-radius: 6px;
  background: linear-gradient(#24262c 0 49.5%, #0d0e11 49.5% 50.5%, #1d1f24 50.5%);
  display: grid; place-items: center; color: #ffd84d;
  font: 700 clamp(1.4rem, 5.6vw, 3.6rem) ui-monospace, Menlo, monospace;
  box-shadow: 0 6px 14px rgba(0,0,0,.5);
}
.cell.flip { animation: flap .14s ease-in; transform-origin: 50% 50%; }
@keyframes flap { from { transform: rotateX(-70deg); filter: brightness(1.8); } to { transform: none; } }
</style>
</head>
<body>
<section class="track">
  <div class="stage">
    <p class="label">Now showing</p>
    <div class="board" id="board"></div>
    <p class="label" id="step">1 / 6</p>
  </div>
</section>
<script>
const words = ['SCROLLBOX', 'DEPARTING', 'GATE 12', 'BOARDING', 'ON TIME', 'ARRIVED'];
const W = 9, CH = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
const board = document.getElementById('board');
const step = document.getElementById('step');
const cells = [];
for (let i = 0; i < W; i++) { const c = document.createElement('div'); c.className = 'cell'; board.appendChild(c); cells.push({ el: c, timer: 0 }); }
function setChar(cell, ch) {
  cell.el.textContent = ch;
  cell.el.classList.remove('flip'); void cell.el.offsetWidth; cell.el.classList.add('flip');
}
function show(word) {
  const target = word.padEnd(W, ' ');
  cells.forEach((cell, i) => {
    clearInterval(cell.timer);
    let spins = 3 + Math.floor(Math.random() * 4);
    setTimeout(() => {
      cell.timer = setInterval(() => {
        if (spins-- <= 0) { clearInterval(cell.timer); setChar(cell, target[i]); return; }
        setChar(cell, CH[Math.floor(Math.random() * CH.length)]);
      }, 70);
    }, i * 45);
  });
}
const track = document.querySelector('.track');
let current = -1;
function update() {
  const r = track.getBoundingClientRect();
  const p = Math.min(0.9999, Math.max(0, -r.top / (r.height - innerHeight)));
  const idx = Math.floor(p * words.length);
  if (idx !== current) { current = idx; show(words[idx]); step.textContent = (idx + 1) + ' / ' + words.length; }
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>

More page turn effects