Slide-over
The next view slides in over the current one, which stays put and dims as it’s covered, like a card laid on a stack. Swipe back and the top view slides away to uncover it again. Tap or swipe the demo to try it.
How to use it
- Paste the snippet just before
</head>. - Add
data-slide-overto a container whose children are the views, one after another.
Options
Add any of these attributes to the same element.
data-direction | Which way views travel: left, right, up or down. Default left. |
data-duration | Time for each slide. Default 700 ms. |
data-interval | Time between automatic slides. Default 2600 ms; 0 turns them off. |
Tap or click to move on to the next view, or swipe back the other way for the previous one; automatic slides rest for a few seconds after that. Call animacious.slideOver(el, 1) or (el, -1) to drive it from your own buttons. With reduced motion the views simply swap.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Slide-over: a section transition. The next view slides in over the
// current one, which stays put and dims into the background as it is
// covered, like a card laid on top of a stack. Going back slides the top
// view away to uncover the one beneath.
// Usage: <div data-slide-over><div>One</div><div>Two</div>…</div>
// Each child is a view. Tap or click to slide the next one over; swipe the
// other way to slide back.
// Optional: data-direction ("left", the default: new views arrive from the
// right and travel left; or "right", "up", "down"), data-duration (ms,
// default 700), data-interval (ms between automatic slides, default 2600;
// "0" turns them off). Automatic slides rest for a while after someone
// interacts.
// Script use: animacious.slideOver(el, 1) for next, (el, -1) for previous.
var DEFAULTS = { direction: 'left', duration: 700, interval: 2600 };
var css =
'.sov-on{position:relative;overflow:hidden;display:grid}' +
'.sov-on>*{grid-area:1/1}' +
'.sov-top{z-index:2;box-shadow:0 0 40px rgba(0,0,0,.45)}' +
'.sov-tap{cursor:pointer;touch-action:pan-y;-webkit-user-select:none;user-select:none}' +
'.sov-tap.sov-y{touch-action:pan-x}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function travel(el) {
var d = el.dataset.direction || DEFAULTS.direction;
return { prop: d === 'up' || d === 'down' ? 'translateY' : 'translateX', sign: d === 'right' || d === 'down' ? 1 : -1 };
}
function go(el, step) {
var s = el._sov;
if (!s || s.busy || s.slides.length < 2) return;
var from = s.slides[s.i];
s.i = (s.i + step + s.slides.length) % s.slides.length;
var to = s.slides[s.i];
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
from.style.visibility = 'hidden';
to.style.visibility = 'visible';
return;
}
var t = travel(el), forward = step > 0;
// Forward: the new view moves over the still one. Back: the current view
// moves away off the top of the old one.
var mover = forward ? to : from, still = forward ? from : to;
var off = -t.sign * 100 + '%';
var move = {}, dim = {};
move[t.prop] = forward ? [off, '0%'] : ['0%', off];
move.duration = dim.duration = s.duration;
move.ease = 'inOut(4)';
dim.filter = forward ? ['brightness(1)', 'brightness(0.55)'] : ['brightness(0.55)', 'brightness(1)'];
dim.ease = 'inOut(2)';
s.busy = true;
to.style.visibility = 'visible';
mover.classList.add('sov-top');
anime.createTimeline({
onComplete: function () {
from.style.visibility = 'hidden';
mover.classList.remove('sov-top');
anime.utils.set([from, to], { translateX: 0, translateY: 0, filter: 'none' });
s.busy = false;
}
}).add(mover, move, 0).add(still, dim, 0);
}
function setup(el) {
el.classList.add('sov-on');
var s = el._sov = {
slides: Array.from(el.children),
i: 0,
busy: false,
duration: +el.dataset.duration || DEFAULTS.duration,
touched: 0
};
s.slides.forEach(function (c, n) { c.style.visibility = n ? 'hidden' : 'visible'; });
// Taps and swipes: not inside a link (a home-page card), so the card's
// own click still works there.
if (!el.closest('a')) {
el.classList.add('sov-tap');
if (travel(el).prop === 'translateY') el.classList.add('sov-y');
var x0 = null, y0 = 0;
el.addEventListener('pointerdown', function (e) { x0 = e.clientX; y0 = e.clientY; });
el.addEventListener('pointerup', function (e) {
if (x0 === null) return;
var t = travel(el), d = t.prop === 'translateX' ? e.clientX - x0 : e.clientY - y0;
var across = t.prop === 'translateX' ? e.clientY - y0 : e.clientX - x0;
x0 = null;
if (Math.abs(across) > 40 && Math.abs(across) > Math.abs(d)) return;
s.touched = Date.now();
if (Math.abs(d) > 40) go(el, (d > 0 ? 1 : -1) === t.sign ? 1 : -1);
else if (Math.abs(d) < 10) go(el, 1);
});
el.addEventListener('pointercancel', function () { x0 = null; });
}
var interval = el.dataset.interval === undefined ? DEFAULTS.interval : +el.dataset.interval;
if (!interval || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
var timer = anime.createTimer({
duration: interval + s.duration,
loop: true,
autoplay: false,
onLoop: function () {
if (el.closest('[hidden]') || Date.now() - s.touched < interval * 2) return;
go(el, 1);
}
});
new IntersectionObserver(function (entries) {
if (entries[0].isIntersecting) timer.play(); else timer.pause();
}).observe(el);
}
window.animacious = window.animacious || {};
window.animacious.slideOver = go;
function init() {
document.querySelectorAll('[data-slide-over]').forEach(function (el) {
if (el._sov || el.children.length < 2) return;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-slide-over>
<section>First view…</section>
<section>Second view…</section>
<section>Third view…</section>
</div>