Magazine page turn
A two-page magazine spread whose leaves turn one by one, with a shadow that follows the curl.
A folded letter opens panel by panel, zig-zag style, as you scroll it open.
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.
<!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>