Tri-fold Leaflet
A sheet printed in three panels folds into thirds like a leaflet: the right panel tucks in over the middle, then the left closes over both to show the cover. After a pause it opens out flat again. The panels really turn on their creases, and darken as they turn from the light.
Day oneCliff path
Hidden coves
Plan your dayHarbourLighthouseTea roomSunset
Visit theCoast
How to use it
- Paste the snippet just before
</head>. - Add
data-tri-fold-leafletto a box with a width and a height, about twice as wide as it is tall. - Put three children in it for the inside panels, left to right, and an optional fourth for the cover.
Options
Add any of these attributes to the same element.
data-loop | Fold and open again and again. |
data-duration | How long each panel takes to fold. Default 900 ms. |
data-hold | How long it stays folded, and open, between moves. Default 1400 ms. |
data-paper | Colour of the plain backs of the panels. Default #faf7ff. |
It starts when scrolled into view. Without data-loop it folds and opens once, and a click or tap does it again. With reduced motion it simply lies open.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Tri-fold Leaflet: a sheet in three panels folds into thirds like a
// leaflet (the right panel tucks in, the left closes over it to show the
// cover), then opens out again. Made with CSS 3D, so the panels really
// turn on their creases.
// Usage:
// <div data-tri-fold-leaflet style="width: 480px; aspect-ratio: 15 / 8">
// <div>Left panel</div><div>Middle panel</div><div>Right panel</div>
// <div>Cover</div> <!-- optional: the outside of the left panel -->
// </div>
// Optional: data-loop (fold and open forever), data-duration (ms per fold,
// default 900), data-hold (ms it stays folded or open, default 1400),
// data-paper (colour of the plain backs, default #faf7ff).
// A click or tap folds it and opens it again when it isn't looping.
var DEFAULTS = { duration: 900, hold: 1400, paper: '#faf7ff' };
var css =
'.tfl{position:relative;perspective:1200px;-webkit-tap-highlight-color:transparent}' +
'.tfl-sheet{position:absolute;inset:0;transform-style:preserve-3d}' +
'.tfl-mid,.tfl-flap{position:absolute;top:0;bottom:0;width:33.334%;transform-style:preserve-3d}' +
'.tfl-mid{left:33.333%}' +
'.tfl-flap.r{left:66.666%;transform-origin:0 50%}' +
'.tfl-flap.l{left:0;transform-origin:100% 50%}' +
'.tfl-face{position:absolute;inset:0;box-sizing:border-box;overflow:hidden;backface-visibility:hidden;-webkit-backface-visibility:hidden}' +
'.tfl-face.back{transform:rotateY(180deg)}' +
'.tfl-shade{position:absolute;inset:0;pointer-events:none;opacity:0}' +
'.tfl-flap.r .tfl-shade{background:linear-gradient(90deg,rgba(30,10,60,.15),rgba(30,10,60,.45))}' +
'.tfl-flap.l .tfl-shade{background:linear-gradient(270deg,rgba(30,10,60,.15),rgba(30,10,60,.45))}' +
'.tfl-mid>.tfl-face::after{content:"";position:absolute;inset:0;pointer-events:none;' +
'background:linear-gradient(90deg,rgba(30,10,60,.08),transparent 6%,transparent 94%,rgba(30,10,60,.08))}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function face(content, back, paper) {
var f = document.createElement('div');
f.className = 'tfl-face' + (back ? ' back' : '');
if (content) f.appendChild(content);
else f.style.background = paper;
var shade = document.createElement('div');
shade.className = 'tfl-shade';
f.appendChild(shade);
return f;
}
function setup(el) {
var d = el.dataset;
var kids = Array.from(el.children);
if (kids.length < 3) return;
var paper = d.paper || DEFAULTS.paper;
el.classList.add('tfl');
var sheet = document.createElement('div');
sheet.className = 'tfl-sheet';
kids.forEach(function (k) { k.style.position = 'absolute'; k.style.inset = '0'; k.style.boxSizing = 'border-box'; });
var mid = document.createElement('div');
mid.className = 'tfl-mid';
mid.appendChild(face(kids[1], false, paper));
var right = document.createElement('div');
right.className = 'tfl-flap r';
right.appendChild(face(kids[2], false, paper));
right.appendChild(face(null, true, paper));
var left = document.createElement('div');
left.className = 'tfl-flap l';
left.appendChild(face(kids[0], false, paper));
left.appendChild(face(kids[3] || null, true, paper));
sheet.appendChild(mid);
sheet.appendChild(right);
sheet.appendChild(left);
el.appendChild(sheet);
var shades = {
rf: right.children[0].lastChild, rb: right.children[1].lastChild,
lf: left.children[0].lastChild, lb: left.children[1].lastChild
};
// r and l run from 0 (flat) to 1 (folded in).
var s = { r: 0, l: 0 };
function draw() {
// The hinges are lifted 1px and 2px so the folded panels stack cleanly.
right.style.transform = 'translateZ(1px) rotateY(' + (-180 * s.r) + 'deg)';
left.style.transform = 'translateZ(2px) rotateY(' + (180 * s.l) + 'deg)';
sheet.style.transform = 'rotateX(16deg) rotateY(' + (-14 + 20 * s.l) + 'deg)';
// Panels darken as they turn away from the light.
shades.rf.style.opacity = Math.sin(s.r * Math.PI / 2) * 0.9;
shades.rb.style.opacity = (1 - s.r) * 0.6;
shades.lf.style.opacity = Math.sin(s.l * Math.PI / 2) * 0.9;
shades.lb.style.opacity = Math.cos(s.l * Math.PI / 2) * 0.6 * (1 - s.l);
}
draw();
if (reduced()) return;
var duration = d.duration !== undefined ? +d.duration : DEFAULTS.duration;
var hold = d.hold !== undefined ? +d.hold : DEFAULTS.hold;
var loop = d.loop !== undefined && d.loop !== 'false';
var tl = null;
function play() {
if (tl) tl.pause();
s.r = 0; s.l = 0;
tl = anime.createTimeline({ loop: loop, loopDelay: hold, onUpdate: draw, defaults: { duration: duration, ease: 'inOut(3)' } })
.add(s, { r: 1 }, 300)
.add(s, { l: 1 }, '-=' + duration * 0.25)
.add(s, { l: 0 }, '+=' + hold)
.add(s, { r: 0 }, '-=' + duration * 0.25);
}
el._tflPlay = play;
el._tflPause = function (on) { if (tl && loop) { if (on) tl.pause(); else tl.play(); } };
if (!loop) {
el.style.cursor = 'pointer';
el.addEventListener('click', function () { if (!tl || tl.completed) play(); });
}
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var el = e.target;
if (!el._tflPlay) return;
if (!e.isIntersecting) el._tflPause(true);
else if (!el._tflStarted) { el._tflStarted = true; el._tflPlay(); }
else el._tflPause(false);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-tri-fold-leaflet]').forEach(function (el) {
if (el._tfl) return;
el._tfl = true;
setup(el);
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-tri-fold-leaflet data-loop style="width: 480px; aspect-ratio: 15 / 8">
<div class="panel">Day one</div>
<div class="panel">Hidden coves</div>
<div class="panel">Plan your day</div>
<div class="cover">Visit the Coast</div>
</div>