Accordion paper fold

A folded letter opens panel by panel, zig-zag style, as you scroll it open.

How it works

Each panel is nested inside the previous one and hinged on its top edge. Odd panels rotate one way and even panels the other, so the chain folds into a zig-zag. One value from scroll progress sets every angle, and shading darkens panels as they tilt away from the light.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Accordion paper fold</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: #2f3e46; color: #354f52; }
.track { height: 300vh; }
.stage { position: sticky; top: 0; height: 100vh; display: grid; justify-items: center; align-content: start; padding-top: 12vh; perspective: 1400px; }
.letter { width: min(80vw, 480px); transform-style: preserve-3d; }
.panel { position: relative; height: 17vh; transform-origin: 50% 0; transform-style: preserve-3d; }
.face { position: absolute; inset: 0; background: #f6f1e7; padding: 16px 24px; font: 1rem/1.5 Georgia, serif; overflow: hidden;
  background-image: repeating-linear-gradient(transparent 0 23px, #e3dccd 23px 24px); }
.face::after { content: ''; position: absolute; inset: 0; background: #000; opacity: var(--shade, 0); pointer-events: none; }
.panel > .panel { position: absolute; top: 100%; left: 0; right: 0; }
.face h3 { margin: 0 0 4px; font-size: 1.3rem; }
</style>
</head>
<body>
<section class="track">
  <div class="stage">
    <div class="letter" id="letter"></div>
  </div>
</section>
<script>
const lines = ['<h3>Dear reader,</h3>Thank you for scrolling this far.', 'Letters used to arrive folded in three or four.', 'Opening one was a small ceremony, a fold at a time.', 'This one opens as fast as you read it.', 'Fold it back up by scrolling the other way.', 'Yours, the box.'];
const letter = document.getElementById('letter');
let holder = letter;
const panels = lines.map(html => {
  const p = document.createElement('div');
  p.className = 'panel';
  p.innerHTML = '<div class="face">' + html + '</div>';
  holder.appendChild(p);
  holder = p;
  return p;
});
const track = document.querySelector('.track');
function update() {
  const r = track.getBoundingClientRect();
  const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
  panels.forEach((el, i) => {
    if (i === 0) return;
    const local = Math.min(1, Math.max(0, p * 1.3 - (i - 1) * 0.12));
    const a = (1 - local) * 165 * (i % 2 ? -1 : 1);
    el.style.transform = 'rotateX(' + a.toFixed(1) + 'deg)';
    el.firstChild.style.setProperty('--shade', (Math.abs(Math.sin(a * Math.PI / 180)) * 0.35).toFixed(3));
  });
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>

More page turn effects