Magazine page turn
A two-page magazine spread whose leaves turn one by one, with a shadow that follows the curl.
Sheets of a spiral-bound notepad lift towards you and flip over the top binding.
Sheets hang from the top edge with transform-origin: top. Each sheet’s slice of the scroll rotates it around the X axis from 0 to 175°, which first brings its bottom edge towards the viewer and then over the binding. The back face shows plain paper once it passes 90°, and the sheet fades out as it clears the top.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Notepad flip-up</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: #cdb4db; color: #2d2a32; }
.track { height: 460vh; }
.stage { position: sticky; top: 0; height: 100vh; display: grid; place-items: center; perspective: 1300px; padding-top: 18vh; }
.pad { position: relative; width: min(76vw, 420px); aspect-ratio: .78; transform-style: preserve-3d; background: #6d597a; border-radius: 6px; box-shadow: 0 30px 50px -20px rgba(45,42,50,.6); }
.spiral { position: absolute; top: -14px; left: 6%; right: 6%; height: 28px; z-index: 100; transform: translateZ(40px);
background: radial-gradient(ellipse 6px 13px at 50% 50%, transparent 4px, #c0c0c8 5px 7px, transparent 8px) 0 0 / 22px 28px repeat-x; }
.sheet { position: absolute; inset: 0; transform-origin: 50% 0; transform-style: preserve-3d; will-change: transform; }
.sheet div { position: absolute; inset: 0; backface-visibility: hidden; -webkit-backface-visibility: hidden; border-radius: 0 0 6px 6px; }
.front { background: #fff9c4; padding: 40px 28px 20px 52px; font: 1.35rem/32px 'Comic Sans MS', 'Segoe Print', cursive;
background-image: linear-gradient(90deg, transparent 40px, #f28482 40px 42px, transparent 42px), repeating-linear-gradient(transparent 0 31px, #a2d2ff 31px 32px); background-position: 0 0, 0 40px; }
.back { background: #fff6b0; transform: rotateX(180deg); }
</style>
</head>
<body>
<section class="track">
<div class="stage"><div class="pad" id="pad"><div class="spiral"></div></div></div>
</section>
<script>
const notes = ['To do:<br>– buy milk<br>– learn scroll-driven animations<br>– call mum', 'Ideas<br>a site that is just a box<br>you scroll forever?', 'Monday<br>stand-up 10:00<br>review 15:30', 'Shopping<br>coffee, oranges, tape', 'Remember:<br>slow down.<br>read the page.', 'Blank page.<br>Start something.'];
const pad = document.getElementById('pad');
const sheets = notes.map((html, i) => {
const s = document.createElement('div');
s.className = 'sheet';
s.innerHTML = '<div class="front">' + html + '</div><div class="back"></div>';
pad.appendChild(s);
return s;
});
const track = document.querySelector('.track');
const n = sheets.length - 1;
function update() {
const r = track.getBoundingClientRect();
const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
sheets.forEach((s, i) => {
const t = i === n ? 0 : Math.min(1, Math.max(0, p * n - i));
const e = t * t * (3 - 2 * t);
s.style.transform = 'rotateX(' + (e * 175).toFixed(1) + 'deg) translateZ(' + (n - i + (t > 0 ? 30 : 0)) + 'px)';
s.style.zIndex = t > 0.5 ? 50 + i : 50 - i;
s.style.opacity = t > 0.7 ? (1 - (t - 0.7) / 0.3).toFixed(2) : 1;
});
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>