Curtain Wipe
A page transition: a coloured curtain sweeps up to cover the page when a link is clicked, and lifts away when the next page opens. This page uses it, so try any link.
How to use it
- Add the snippet to every page of your site.
- Add
data-curtain-wipeto each page’s<body>.
Options
Add any of these attributes to the same element.
data-curtain-wipe="#hex" | Curtain colour. Default #6a0dad. |
data-duration | Time for each sweep. Default 650 ms. |
data-interval | Contained mode only: time each slide stays. Default 2400 ms. |
Links to other sites, new tabs, downloads and #anchors are left alone. Put data-curtain-wipe on any other element with two or more children and it becomes a slideshow that wipes between them.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Curtain Wipe: a page transition. A coloured curtain sweeps up to cover the
// page when a link is clicked, and lifts away again when the next page opens.
// Usage: <body data-curtain-wipe> (add the snippet to every page)
// Optional: data-curtain-wipe="#6a0dad" (curtain colour), data-duration (ms).
// Contained mode: put data-curtain-wipe on any other element with two or
// more children and it wipes between them every data-interval ms (a slideshow).
var DEFAULTS = { color: '#6a0dad', duration: 650, interval: 2400 };
var css =
'.cw-panel{position:fixed;inset:0;z-index:2147483000;pointer-events:none;transform:scaleY(0)}' +
'.cw-contained{position:relative;overflow:hidden;display:grid}' +
'.cw-contained>*:not(.cw-panel){grid-area:1/1}' +
'.cw-contained>.cw-panel{position:absolute}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function panel(parent, color) {
var p = document.createElement('div');
p.className = 'cw-panel';
p.setAttribute('aria-hidden', 'true');
p.style.background = color;
parent.appendChild(p);
return p;
}
// Cover: grows up from the bottom. Reveal: shrinks away upwards.
function cover(p, duration, done) {
anime.utils.set(p, { transformOrigin: '50% 100%' });
anime.animate(p, { scaleY: [0, 1], duration: duration, ease: 'inOut(3)', onComplete: done });
}
function reveal(p, duration, done) {
anime.utils.set(p, { transformOrigin: '50% 0%' });
anime.animate(p, { scaleY: [1, 0], duration: duration, ease: 'inOut(3)', onComplete: done });
}
function pageMode(body) {
var color = body.getAttribute('data-curtain-wipe') || DEFAULTS.color;
var duration = +body.dataset.duration || DEFAULTS.duration;
var p = panel(body, color);
anime.utils.set(p, { scaleY: 1 });
reveal(p, duration);
document.addEventListener('click', function (e) {
var a = e.target.closest && e.target.closest('a[href]');
if (!a || e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
if (a.target && a.target !== '_self') return;
if (a.hasAttribute('download')) return;
var url = new URL(a.href, location.href);
if (url.origin !== location.origin) return;
// Same page, only the #hash changes: let the browser scroll.
if (url.pathname === location.pathname && url.search === location.search && url.hash) return;
e.preventDefault();
cover(p, duration, function () { location.href = url.href; });
});
// Coming back with the Back button can restore the covered page: lift it.
window.addEventListener('pageshow', function (e) {
if (e.persisted) anime.utils.set(p, { scaleY: 0 });
});
}
function containedMode(el) {
el.classList.add('cw-contained');
var slides = Array.from(el.children);
var color = el.getAttribute('data-curtain-wipe') || DEFAULTS.color;
var duration = +el.dataset.duration || DEFAULTS.duration;
var p = panel(el, color);
var i = 0;
slides.forEach(function (s, n) { s.style.visibility = n ? 'hidden' : 'visible'; });
setInterval(function () {
cover(p, duration, function () {
slides[i].style.visibility = 'hidden';
i = (i + 1) % slides.length;
slides[i].style.visibility = 'visible';
reveal(p, duration);
});
}, (+el.dataset.interval || DEFAULTS.interval) + duration * 2);
}
function init() {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
document.querySelectorAll('[data-curtain-wipe]').forEach(function (el) {
if (el._cw) return;
el._cw = true;
if (el === document.body) pageMode(el);
else if (el.children.length > 1) containedMode(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><body data-curtain-wipe="#6a0dad">
…
</body>