Tear-off calendar
Daily calendar sheets rip away and tumble off the pad, one day per stretch of scrolling.
A two-page magazine spread whose leaves turn one by one, with a shadow that follows the curl.
Each leaf is a 3D card hinged on the spine with transform-origin on its left edge. Its slice of scroll progress is eased and turned into rotateY from 0 to −180°. The same value drives sin(t·π) shading on both faces and a cast shadow on the page underneath, so the light is strongest mid-turn.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Magazine page turn</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: #2c2a35; color: #1f1b16; }
.track { height: 420vh; }
.stage { position: sticky; top: 0; height: 100vh; display: grid; place-items: center; perspective: 2200px; }
.mag { position: relative; width: min(88vw, 940px); aspect-ratio: 1.45; }
.base, .side { padding: 6%; font-family: Georgia, serif; }
.base { position: absolute; top: 0; bottom: 0; width: 50%; background: #f3efe6; }
.left { left: 0; border-radius: 6px 0 0 6px; box-shadow: inset -18px 0 30px -18px rgba(0,0,0,.35); }
.right { right: 0; border-radius: 0 6px 6px 0; box-shadow: inset 18px 0 30px -18px rgba(0,0,0,.35); }
.base ol { padding-left: 1.2em; font-size: clamp(.8rem, 1.6vw, 1.1rem); line-height: 2; }
.cast { position: absolute; inset: 0; background: linear-gradient(90deg, rgba(0,0,0,.45), transparent 70%); opacity: 0; pointer-events: none; border-radius: inherit; }
.leaf { position: absolute; left: 50%; top: 0; width: 50%; height: 100%; transform-origin: 0 50%; transform-style: preserve-3d; will-change: transform; }
.side { position: absolute; inset: 0; backface-visibility: hidden; -webkit-backface-visibility: hidden; display: flex; flex-direction: column; justify-content: flex-end; overflow: hidden; border-radius: 0 6px 6px 0; }
.side::after { content: ''; position: absolute; inset: 0; pointer-events: none; background: linear-gradient(90deg, rgba(0,0,0,var(--sh, 0)), rgba(255,255,255,calc(var(--sh, 0) * .4)) 55%, transparent); }
.back { transform: rotateY(180deg); border-radius: 6px 0 0 6px; }
.back::after { background: linear-gradient(270deg, rgba(0,0,0,var(--sh, 0)), transparent 70%); }
small { font: 700 .7rem system-ui, sans-serif; letter-spacing: .12em; text-transform: uppercase; opacity: .7; }
h3 { font-size: clamp(1.1rem, 3.4vw, 2.6rem); line-height: 1.05; margin: .3em 0; }
.side p { margin: 0; font-size: clamp(.75rem, 1.5vw, 1rem); }
.c1 { background: linear-gradient(160deg, #ff6f59 0 55%, #f3efe6 55%); }
.c2 { background: #f3efe6; }
.c3 { background: radial-gradient(circle at 70% 30%, #ffd166 0 22%, transparent 23%), #118ab2; color: #fff; }
.c4 { background: #f3efe6; }
.c5 { background: repeating-linear-gradient(120deg, #2b2d42 0 18px, #3d405b 18px 36px); color: #fff; }
.c6 { background: #f3efe6; }
</style>
</head>
<body>
<section class="track">
<div class="stage">
<div class="mag" id="mag">
<div class="base left"><small>Contents</small><ol><li>The scroll issue</li><li>Inside the box</li><li>Slow pages</li><li>Back matter</li></ol></div>
<div class="base right"><small>Back cover</small><h3>See you next issue.</h3><div class="cast" id="cast"></div></div>
<div class="leaf"><div class="side front c1"><small>Issue 07</small><h3>The scroll issue</h3><p>Everything below the fold.</p></div><div class="side back c2"><small>Essay</small><h3>Why we turn pages</h3><p>A short history of the flick.</p></div></div>
<div class="leaf"><div class="side front c3"><small>Feature</small><h3>Inside the box</h3><p>Where content waits.</p></div><div class="side back c4"><small>Interview</small><h3>Slow pages</h3><p>On reading at the speed of a thumb.</p></div></div>
<div class="leaf"><div class="side front c5"><small>Photo</small><h3>Paper light</h3><p>Shadows do the work.</p></div><div class="side back c6"><small>Notes</small><h3>Back matter</h3><p>Credits and corrections.</p></div></div>
</div>
</div>
</section>
<script>
const leaves = [...document.querySelectorAll('.leaf')];
const cast = document.getElementById('cast');
const track = document.querySelector('.track');
const n = leaves.length;
const ease = t => t * t * (3 - 2 * t);
function update() {
const r = track.getBoundingClientRect();
const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
let castMax = 0;
leaves.forEach((leaf, i) => {
const t = ease(Math.min(1, Math.max(0, p * n - i)));
const sh = Math.sin(t * Math.PI);
leaf.style.transform = 'rotateY(' + (-180 * t).toFixed(2) + 'deg)';
leaf.style.setProperty('--sh', (sh * 0.45).toFixed(3));
leaf.style.zIndex = t > 0.5 ? 10 + i : 10 + n - i;
castMax = Math.max(castMax, sh);
});
cast.style.opacity = (castMax * 0.8).toFixed(3);
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>