Push-back Menu
Tap the menu button and the whole page slides aside, shrinking back and turning slightly away as if it were being pushed into the distance. Behind it is the menu, its links sliding in one after another. Tap the page to bring it forward again.
Animacious
How to use it
- Paste the snippet just before
</head>. - Add
data-push-back-menuto an element holding two things: a<nav>(the menu behind) and a<div data-page>(your page). - Put an empty
<button data-toggle aria-label="Menu"></button>in the page and the script draws a menu icon in it. Without one it adds a button at the top of the page. - For a whole site, make the wrapper fill the window (
position: fixed; inset: 0) and let the page scroll inside it.
Options
Add any of these attributes to the same element.
data-side | Which side the menu is on: left or right. Default left. |
data-shift | How far the page slides, as a share of the width (0.2 to 0.9). Default 0.6. |
data-scale | How small the page shrinks. Default 0.82. |
data-bg | Colour behind the page. Default #1e0b36. |
data-color | Colour of the links. Default #fff. |
data-autoplay | It opens and closes itself while in view, pausing when someone taps. Handy for demos. |
The button gets aria-expanded and the closed menu is out of the tab order. Escape, tapping the page or picking a link closes it, and it fires a toggle event (e.detail.open). From code, animacious.pushBackMenu(el) opens or closes it. With reduced motion the page jumps aside without the slide.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Push-back Menu: tap the menu button and the whole page slides aside,
// shrinking back and turning slightly away as if pushed into the
// distance, uncovering the menu that was waiting behind it. The links
// slide in one after another. Tap the page (or the button) to bring it
// back.
// Usage: <div data-push-back-menu>
// <nav><a href="/">Home</a><a href="/work">Work</a>…</nav>
// <div data-page>
// <button data-toggle aria-label="Menu"></button>
// …your page…
// </div>
// </div>
// The nav is the menu behind; [data-page] (or the other child) is the page.
// An empty [data-toggle] gets a two-line menu icon; without one the script
// adds a button at the top of the page.
// Optional: data-side (where the menu is: left or right, default left),
// data-shift (how much of the width the page slides by, 0 to 1, default
// 0.6), data-scale (how small the page shrinks, default 0.82), data-bg
// (colour behind the page, default #1e0b36), data-color (link colour,
// default #fff), data-autoplay (it opens and closes itself while in view;
// handy for demos).
// Escape, tapping the page or picking a link closes it. Listen for the
// "toggle" event: e.detail.open is true or false.
// Script use: animacious.pushBackMenu(el) opens or closes it.
var DEFAULTS = { side: 'left', shift: 0.6, scale: 0.82, bg: '#1e0b36', color: '#fff', duration: 700, stagger: 60 };
var uid = 0;
var css =
'.pbm{position:relative;overflow:hidden;background:var(--pbm-bg);perspective:1400px}' +
'.pbm-menu{position:absolute;top:0;bottom:0;left:0;width:calc(var(--pbm-shift) * 100%);display:flex;flex-direction:column;justify-content:center;gap:.2em;padding:24px 24px 24px 8%;box-sizing:border-box;color:var(--pbm-color);visibility:hidden}' +
'.pbm-right .pbm-menu{left:auto;right:0;align-items:flex-end;padding:24px 8% 24px 24px}' +
'.pbm-menu>*{color:inherit;text-decoration:none;font-weight:700;letter-spacing:-.02em;line-height:1.2}' +
'.pbm-page{position:relative;z-index:1;min-height:100%;transform-origin:0 50%;will-change:transform}' +
'.pbm-right .pbm-page{transform-origin:100% 50%}' +
'.pbm-open .pbm-page{cursor:pointer}' +
'.pbm-open .pbm-page>*{pointer-events:none}' +
'.pbm-btn{position:relative;display:inline-block;width:40px;height:40px;padding:0;border:0;border-radius:10px;background:none;color:inherit;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
'.pbm-line{position:absolute;left:11px;right:11px;top:50%;height:2px;margin-top:-1px;border-radius:2px;background:currentColor;pointer-events:none}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function icon(s, open, still) {
var to = open ? [{ y: 0, rotate: 45 }, { y: 0, rotate: -45 }] : [{ y: -4, rotate: 0 }, { y: 4, rotate: 0 }];
s.lines.forEach(function (l, k) {
if (still) anime.utils.set(l, to[k]);
else anime.animate(l, Object.assign({ duration: 400, ease: 'inOut(3)' }, to[k]));
});
}
function pose(s, open) {
var dir = s.side === 'right' ? -1 : 1;
return open
? { x: dir * s.shift * s.el.offsetWidth, scale: s.scale, rotateY: -dir * 8, borderRadius: 18, boxShadow: '0 30px 70px rgba(0,0,0,.45)' }
: { x: 0, scale: 1, rotateY: 0, borderRadius: 0, boxShadow: '0 0px 0px rgba(0,0,0,0)' };
}
function toggle(el) {
var s = el._pbm;
var open = (s.open = !s.open);
el.classList.toggle('pbm-open', open);
if (s.a11y) {
s.btn.setAttribute('aria-expanded', open);
s.btn.setAttribute('aria-label', open ? 'Close menu' : 'Menu');
if (open) s.menu.removeAttribute('inert'); else s.menu.setAttribute('inert', '');
}
el.dispatchEvent(new CustomEvent('toggle', { bubbles: true, detail: { open: open } }));
anime.utils.remove([s.page].concat(s.links));
var still = reduced();
icon(s, open, still);
var to = pose(s, open);
var dir = s.side === 'right' ? 1 : -1;
if (open) {
s.menu.style.visibility = 'visible';
if (still) { anime.utils.set(s.page, to); anime.utils.set(s.links, { opacity: 1, x: 0 }); return; }
anime.animate(s.page, Object.assign({ duration: s.duration, ease: 'inOut(4)' }, to));
anime.animate(s.links, { opacity: [0, 1], x: [dir * 40, 0], duration: 520, delay: anime.stagger(s.stagger, { start: s.duration * 0.4 }), ease: 'out(3)' });
} else {
var hide = function () { if (!s.open) s.menu.style.visibility = 'hidden'; };
if (still) { anime.utils.set(s.page, to); hide(); return; }
anime.animate(s.links, { opacity: 0, x: dir * 24, duration: 240, delay: anime.stagger(30, { from: 'last' }), ease: 'in(2)' });
anime.animate(s.page, Object.assign({ duration: s.duration * 0.85, delay: 80, ease: 'inOut(4)', onComplete: hide }, to));
}
}
function setup(el) {
var d = el.dataset;
var menu = el.querySelector('nav') || el.querySelector('[data-menu]');
var page = el.querySelector('[data-page]') || Array.prototype.filter.call(el.children, function (c) { return c !== menu; })[0];
if (!menu || !page) return;
var a11y = !el.closest('[aria-hidden="true"]');
var side = d.side === 'right' ? 'right' : DEFAULTS.side;
var shift = Math.min(0.9, Math.max(0.2, parseFloat(d.shift) || DEFAULTS.shift));
el.classList.add('pbm');
if (side === 'right') el.classList.add('pbm-right');
el.style.setProperty('--pbm-bg', d.bg || DEFAULTS.bg);
el.style.setProperty('--pbm-color', d.color || DEFAULTS.color);
el.style.setProperty('--pbm-shift', shift);
menu.classList.add('pbm-menu');
page.classList.add('pbm-page');
menu.id = menu.id || 'push-back-menu-' + (++uid);
var btn = page.querySelector('[data-toggle]');
if (!btn) {
btn = document.createElement(a11y ? 'button' : 'span');
page.insertBefore(btn, page.firstChild);
}
btn.classList.add('pbm-btn');
var lines = [];
if (!btn.children.length && !btn.textContent.trim()) {
lines = [0, 1].map(function () {
var l = document.createElement('span');
l.className = 'pbm-line';
l.setAttribute('aria-hidden', 'true');
btn.appendChild(l);
return l;
});
}
if (a11y && btn.tagName === 'BUTTON') {
if (!btn.getAttribute('type')) btn.type = 'button';
btn.setAttribute('aria-label', 'Menu');
btn.setAttribute('aria-expanded', 'false');
btn.setAttribute('aria-controls', menu.id);
menu.setAttribute('inert', '');
}
var s = el._pbm = {
el: el, menu: menu, page: page, btn: btn, lines: lines, links: Array.prototype.slice.call(menu.children),
side: side, shift: shift, scale: parseFloat(d.scale) || DEFAULTS.scale,
duration: DEFAULTS.duration, stagger: DEFAULTS.stagger, open: false, a11y: a11y && btn.tagName === 'BUTTON'
};
anime.utils.set(s.links, { opacity: 0 });
icon(s, false, true);
btn.addEventListener('click', function (e) {
e.stopPropagation();
toggle(el);
});
// While pushed back, a tap anywhere on the page brings it forward again.
page.addEventListener('click', function (e) {
if (!s.open || btn.contains(e.target)) return;
e.preventDefault();
toggle(el);
}, true);
menu.addEventListener('click', function (e) {
var a = e.target.closest('a');
if (!a || !s.open) return;
if (a.getAttribute('href') === '#') e.preventDefault();
toggle(el);
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && s.open) { toggle(el); if (s.a11y) btn.focus(); }
});
// Keep an open page at the right distance when the width changes.
window.addEventListener('resize', function () {
if (!s.open) return;
anime.utils.remove(s.page);
anime.utils.set(s.page, pose(s, true));
});
if (d.autoplay !== undefined && d.autoplay !== 'false' && !reduced()) {
var timer = null, idleUntil = 0;
el.addEventListener('pointerdown', function () { idleUntil = Date.now() + 8000; });
var tick = function () {
if (Date.now() > idleUntil && !menu.contains(document.activeElement)) toggle(el);
timer = setTimeout(tick, s.open ? 2600 : 1500);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(tick, 800);
}).observe(el);
}
}
function init() {
document.querySelectorAll('[data-push-back-menu]').forEach(function (el) {
if (el._pbm) return;
setup(el);
});
}
window.animacious = window.animacious || {};
window.animacious.pushBackMenu = function (el) { if (el._pbm) toggle(el); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-push-back-menu>
<nav>
<a href="/">Home</a>
<a href="/work">Work</a>
<a href="/about">About</a>
</nav>
<div data-page>
<button data-toggle aria-label="Menu"></button>
…your page…
</div>
</div>