Magazine page turn
A two-page magazine spread whose leaves turn one by one, with a shadow that follows the curl.
Daily calendar sheets rip away and tumble off the pad, one day per stretch of scrolling.
Sheets are stacked in a pinned pad, first day on top. Each sheet owns a slice of the scroll; inside it, the sheet rotates around its top-left corner, then falls with an accelerating translateY so it looks pulled by gravity. Scrolling back reattaches it.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tear-off calendar</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: #e9e4d8; color: #2a2522; overflow-x: hidden; }
.track { height: 480vh; }
.stage { position: sticky; top: 0; height: 100vh; display: grid; place-items: center; overflow: hidden; }
.pad { position: relative; width: min(64vw, 340px); aspect-ratio: .8; }
.rings { position: absolute; top: -12px; left: 8%; right: 8%; height: 24px; z-index: 50;
background: radial-gradient(circle, #8b8580 0 5px, transparent 6px) 0 0 / 28px 24px repeat-x; }
.sheet {
position: absolute; inset: 0; background: #fffdf8; border-radius: 4px 4px 10px 10px;
box-shadow: 0 1px 0 #d8d2c6, 0 10px 30px -12px rgba(0,0,0,.35);
display: flex; flex-direction: column; align-items: center; transform-origin: 8% 0; will-change: transform;
}
.sheet header { width: 100%; background: #d64933; color: #fff; text-align: center; padding: 14px 0 10px; font: 800 1rem system-ui, sans-serif; letter-spacing: .3em; border-radius: 4px 4px 0 0; }
.sheet b { font: 900 clamp(5rem, 22vw, 9rem)/1 system-ui, sans-serif; margin-top: 8%; letter-spacing: -.04em; }
.sheet span { font: 600 1.2rem Georgia, serif; font-style: italic; }
.sheet.sun b { color: #d64933; }
</style>
</head>
<body>
<section class="track">
<div class="stage">
<div class="pad" id="pad"><div class="rings"></div></div>
</div>
</section>
<script>
const pad = document.getElementById('pad');
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const sheets = days.map((d, i) => {
const s = document.createElement('div');
s.className = 'sheet' + (i === 6 ? ' sun' : '');
s.style.zIndex = 40 - i;
s.innerHTML = '<header>NOVEMBER</header><b>' + (9 + i) + '</b><span>' + d + '</span>';
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) => {
if (i === n) return;
const t = Math.min(1, Math.max(0, p * n - i));
const lift = Math.min(1, t / 0.35);
const fall = Math.max(0, (t - 0.35) / 0.65);
s.style.transform = 'translate(' + (-fall * 30) + 'vw,' + (fall * fall * 130) + 'vh) rotate(' + (-lift * 14 - fall * 50) + 'deg)';
s.style.opacity = t >= 1 ? 0 : 1;
});
}
addEventListener('scroll', update, { passive: true });
update();
</script>
</body>
</html>