Flip book

Pages of a pinned book turn one after another as you scroll, each folding over its spine.

How it works

Pages are stacked on the right half of the book with transform-origin on their left edge. Each page owns a slice of the scroll progress and rotates from 0 to −180 degrees inside it. z-index swaps at the halfway point so turned pages stack correctly.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Flip book</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: #3d2c2e; color: #2b2118; }
.track { height: 500vh; }
.stage { position: sticky; top: 0; height: 100vh; display: grid; place-items: center; perspective: 1600px; }
.book { position: relative; width: 80vmin; height: 52vmin; }
.book::before { content: ''; position: absolute; inset: 0 50% 0 0; background: #e8dcc6; border-radius: 8px 0 0 8px; box-shadow: -8px 10px 30px rgba(0,0,0,.4); }
.page { position: absolute; left: 50%; top: 0; width: 50%; height: 100%; transform-origin: 0 50%; transform-style: preserve-3d; }
.page div {
  position: absolute; inset: 0; backface-visibility: hidden; display: grid; place-items: center; padding: 20px; text-align: center;
  font: 600 clamp(1rem, 3.4vmin, 1.8rem)/1.3 Georgia, serif; background: #f5ecd9; border-radius: 0 8px 8px 0;
  box-shadow: inset 12px 0 20px -12px rgba(0,0,0,.35);
}
.page .back { transform: rotateY(180deg); border-radius: 8px 0 0 8px; background: #efe3cb; box-shadow: inset -12px 0 20px -12px rgba(0,0,0,.35); }
</style>
</head>
<body>
<section class="track">
  <div class="stage">
    <div class="book" id="book">
      <div class="page"><div class="front">Chapter one</div><div class="back">The scroll begins.</div></div>
      <div class="page"><div class="front">Chapter two</div><div class="back">Pages fold over.</div></div>
      <div class="page"><div class="front">Chapter three</div><div class="back">Each one in turn.</div></div>
      <div class="page"><div class="front">Chapter four</div><div class="back">Nearly done.</div></div>
      <div class="page"><div class="front">Epilogue</div><div class="back">The end.</div></div>
    </div>
  </div>
</section>
<script>
const pages = [...document.querySelectorAll('.page')];
const track = document.querySelector('.track');
const n = pages.length;
function update() {
  const r = track.getBoundingClientRect();
  const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
  pages.forEach((pg, i) => {
    const t = Math.min(1, Math.max(0, p * n - i));
    pg.style.transform = 'rotateY(' + (-180 * t).toFixed(1) + 'deg)';
    pg.style.zIndex = t > 0.5 ? i : n - i;
  });
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>

More 3d effects