Stair Wipe
A page transition: when a link is clicked, columns of colour drop down one after another like a flight of steps until they cover the page, and on the next page they lift away in the same order. This page uses it, so try any link.
How to use it
- Add the snippet to every page of your site.
- Add
data-stair-wipeto each page’s<body>.
Options
Add any of these attributes to the same element.
data-stair-wipe="#hex" | Column colour. Default #6a0dad. |
data-columns | How many columns. Default 6. |
data-duration | Time for each column to drop or lift. Default 500 ms. |
data-stagger | Gap between one column and the next. Default 70 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 lifts the stairs again. Put data-stair-wipe on any other element with two or more children and it becomes a slideshow.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Stair Wipe: a page transition. When a link is clicked, columns of colour
// drop down one after another like a flight of steps until they cover the
// page; on the next page they lift away in the same order.
// Usage: <body data-stair-wipe> (add the snippet to every page)
// Optional: data-stair-wipe="#hex" (column colour, default #6a0dad),
// data-columns (how many, default 6), data-duration (ms for each column,
// default 500), data-stagger (ms between columns, default 70).
// Contained mode: put data-stair-wipe on any other element with two or more
// children and the stairs run between them every data-interval ms
// (default 2200).
var DEFAULTS = { color: '#6a0dad', columns: 6, duration: 500, stagger: 70, interval: 2200 };
var css =
'.stw-layer{position:fixed;inset:0;z-index:2147483000;pointer-events:none;visibility:hidden;overflow:hidden}' +
'.stw-col{position:absolute;top:0;bottom:0;background-image:linear-gradient(rgba(255,255,255,0) 70%,rgba(255,255,255,.14))}' +
'.stw-contained{position:relative;overflow:hidden;display:grid}' +
'.stw-contained>*:not(.stw-layer){grid-area:1/1}' +
'.stw-contained>.stw-layer{position:absolute}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function layer(parent, color, n) {
var l = document.createElement('div');
l.className = 'stw-layer';
l.setAttribute('aria-hidden', 'true');
for (var i = 0; i < n; i++) {
var c = document.createElement('div');
c.className = 'stw-col';
// A touch wider than its share so neighbours overlap without a gap.
c.style.left = (i * 100 / n) + '%';
c.style.width = 'calc(' + (100 / n) + '% + 1px)';
c.style.backgroundColor = color;
l.appendChild(c);
}
parent.appendChild(l);
l._stwCols = Array.from(l.children);
return l;
}
// Cover: columns drop in from above. Uncover: they lift back up and away.
function stairs(l, cover, o, done) {
if (l._stw) l._stw.pause();
l.style.visibility = 'visible';
l._stw = anime.animate(l._stwCols, {
y: cover ? ['-101%', '0%'] : ['0%', '-101%'],
duration: o.duration,
ease: cover ? 'out(4)' : 'in(3)',
delay: anime.stagger(o.stagger),
onComplete: function () { if (!cover) l.style.visibility = 'hidden'; if (done) done(); }
});
return o.duration + o.stagger * (l._stwCols.length - 1);
}
function options(el) {
return {
color: el.getAttribute('data-stair-wipe') || DEFAULTS.color,
columns: Math.max(2, +el.dataset.columns || DEFAULTS.columns),
duration: +el.dataset.duration || DEFAULTS.duration,
stagger: el.dataset.stagger ? +el.dataset.stagger : DEFAULTS.stagger
};
}
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 o = options(body), l = layer(body, o.color, o.columns);
var trip = 0;
stairs(l, false, o);
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; };
try {
var total = stairs(l, true, o, go);
setTimeout(go, total + 400);
} catch (err) { go(); }
});
window.addEventListener('pageshow', function (e) {
if (!e.persisted) return;
trip++;
stairs(l, false, o);
});
}
function containedMode(el, reduced) {
el.classList.add('stw-contained');
var slides = Array.from(el.children);
var o = options(el), l = layer(el, o.color, o.columns);
var i = 0;
slides.forEach(function (s, n) { s.style.visibility = n ? 'hidden' : 'visible'; });
if (reduced) return;
var each = o.duration + o.stagger * (o.columns - 1);
var timer = anime.createTimer({
duration: (+el.dataset.interval || DEFAULTS.interval) + each * 2 + 120,
loop: true,
autoplay: false,
onLoop: function () {
if (el.closest('[hidden]')) return;
stairs(l, true, o, function () {
slides[i].style.visibility = 'hidden';
i = (i + 1) % slides.length;
slides[i].style.visibility = 'visible';
setTimeout(function () { stairs(l, false, o); }, 120);
});
}
});
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-stair-wipe]').forEach(function (el) {
if (el._stw) return;
el._stw = 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-stair-wipe>
…
</body>