Page Fade
A page transition: the page fades out to its background colour when a link is clicked, and the next page fades in from it. Quiet and smooth, it suits any site. This page uses it, so try any link.
How to use it
- Add the snippet to every page of your site.
- Add
data-page-fadeto each page’s<body>.
Options
Add any of these attributes to the same element.
data-page-fade="#hex" | The colour it fades through. Default: the page’s own background. |
data-duration | Time for each fade. Default 450 ms. |
data-interval | Contained mode only: time each slide stays. Default 2200 ms. |
Links to other sites, new tabs, downloads and #anchors are left alone, and if anything goes wrong the link still opens. Coming back with the Back button fades the page in again. Put data-page-fade on any other element with two or more children and it becomes a slideshow that fades between them.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Page Fade: a page transition. When a link is clicked the page fades out
// to its background colour, and the next page fades in from it. Quiet,
// smooth and suits any site.
// Usage: <body data-page-fade> (add the snippet to every page)
// Optional: data-page-fade="#hex" (the colour it fades through, default the
// page's own background), data-duration (ms, default 450).
// Contained mode: put data-page-fade on any other element with two or more
// children and it fades between them every data-interval ms (a slideshow).
var DEFAULTS = { duration: 450, interval: 2200 };
var css =
'.pfd-cover{position:fixed;inset:0;z-index:2147483000;pointer-events:none;opacity:0;visibility:hidden}' +
'.pfd-contained{position:relative;overflow:hidden;display:grid}' +
'.pfd-contained>*:not(.pfd-cover){grid-area:1/1}' +
'.pfd-contained>.pfd-cover{position:absolute}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
// The colour to fade through: the attribute, else the element's (or the
// page's) own background, else white.
function colorOf(el) {
var c = el.getAttribute('data-page-fade');
if (c) return c;
var nodes = [el, document.documentElement];
for (var i = 0; i < nodes.length; i++) {
var bg = getComputedStyle(nodes[i]).backgroundColor;
if (bg && bg !== 'transparent' && !/rgba\(.*,\s*0\)$/.test(bg)) return bg;
}
return '#fff';
}
function cover(parent, color) {
var c = document.createElement('div');
c.className = 'pfd-cover';
c.setAttribute('aria-hidden', 'true');
c.style.background = color;
parent.appendChild(c);
return c;
}
function fade(c, to, duration, done) {
if (c._pfd) c._pfd.pause();
c.style.visibility = 'visible';
c._pfd = anime.animate(c, {
opacity: to, duration: duration, ease: to ? 'in(2)' : 'out(2)',
onComplete: function () { if (!to) c.style.visibility = 'hidden'; if (done) done(); }
});
}
// Same-site links that should get the transition: not new tabs, downloads,
// other sites or #anchors on this page.
function linkFor(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 null;
if ((a.target && a.target !== '_self') || a.hasAttribute('download')) return null;
var url = new URL(a.href, location.href);
if (url.origin !== location.origin) return null;
if (url.pathname === location.pathname && url.search === location.search && url.hash) return null;
return url;
}
function pageMode(body) {
var duration = +body.dataset.duration || DEFAULTS.duration;
var c = cover(body, colorOf(body));
var trip = 0;
anime.utils.set(c, { opacity: 1 });
fade(c, 0, duration);
document.addEventListener('click', function (e) {
var url = linkFor(e);
if (!url) return;
e.preventDefault();
var t = ++trip, gone = false;
var go = function () { if (gone || t !== trip) return; gone = true; location.href = url.href; };
// Never hold up the visit: go anyway if the fade stalls or fails.
setTimeout(go, duration + 400);
try { fade(c, 1, duration, go); } catch (err) { go(); }
});
// Coming back with the Back button can restore the faded-out page: fade it
// back in, and cancel the visit that page was on its way to.
window.addEventListener('pageshow', function (e) {
if (!e.persisted) return;
trip++;
fade(c, 0, duration);
});
}
function containedMode(el, reduced) {
el.classList.add('pfd-contained');
var slides = Array.from(el.children);
var duration = +el.dataset.duration || DEFAULTS.duration;
var c = cover(el, colorOf(el));
var i = 0;
slides.forEach(function (s, n) { s.style.visibility = n ? 'hidden' : 'visible'; });
if (reduced) return;
// Fade out, swap, fade in: on a timer that only runs while it's on screen.
var timer = anime.createTimer({
duration: (+el.dataset.interval || DEFAULTS.interval) + duration * 2,
loop: true,
autoplay: false,
onLoop: function () {
if (el.closest('[hidden]')) return;
fade(c, 1, duration, function () {
slides[i].style.visibility = 'hidden';
i = (i + 1) % slides.length;
slides[i].style.visibility = 'visible';
setTimeout(function () { fade(c, 0, duration); }, 80);
});
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) timer.play(); else timer.pause();
}).observe(el);
}
function init() {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
document.querySelectorAll('[data-page-fade]').forEach(function (el) {
if (el._pfd) return;
el._pfd = true;
if (el === document.body) { if (!reduced) pageMode(el); }
else if (el.children.length > 1) containedMode(el, reduced);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><body data-page-fade>
…
</body>