Modal Scale-fade
A dialog grows up from slightly small and fades in over a dimmed page, then shrinks away when closed. Built on the native <dialog>, so focus, Esc and screen readers just work.
How to use it
- Paste the snippet just before
</head>. - Add
data-modal-scale-fadeto a<dialog>with anid. - Add
data-modal-open="#that-id"to the button that opens it, anddata-modal-closeto any button inside that closes it.
Options
Add any of these attributes to the same element.
data-duration | Time to open. Default 380 ms. |
data-from | Starting scale. Default 0.9. |
Esc and a click on the dimmed backdrop close it too. From code: animacious.modal(dialog) opens it and animacious.modal(dialog, false) closes it.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Modal Scale-fade: a dialog grows up from slightly small and fades in over
// a dimmed page, then shrinks away when closed. Built on the native
// <dialog>, so focus, Esc and screen readers all just work.
// Usage:
// <button data-modal-open="#signup">Sign up</button>
// <dialog id="signup" data-modal-scale-fade>
// <h2>Join us</h2>
// <button data-modal-close>Close</button>
// </dialog>
// Esc, any [data-modal-close] and a click on the dimmed backdrop close it.
// Optional: data-duration (ms, default 380), data-from (starting scale,
// default 0.9).
// Script use: animacious.modal(dialog) opens it, animacious.modal(dialog,
// false) closes it.
// Contained mode (for previews): put data-modal-scale-fade on a <div>
// instead and it opens inside its parent with its own backdrop.
var DEFAULTS = { duration: 380, from: 0.9 };
var css =
'dialog.msf{margin:auto;border:0;border-radius:16px;padding:28px;max-width:min(440px,calc(100vw - 32px));box-shadow:0 30px 80px rgba(10,10,25,.3)}' +
'dialog.msf::backdrop{background:rgba(10,10,25,.45);-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px)}' +
'dialog.msf.msf-in::backdrop{animation:msf-fade .3s ease-out both}' +
'dialog.msf.msf-out::backdrop{animation:msf-fade .25s ease-in reverse both}' +
'@keyframes msf-fade{from{opacity:0}to{opacity:1}}' +
'.msf-inline{position:absolute;z-index:2;visibility:hidden}' +
'.msf-backdrop{position:absolute;inset:0;z-index:1;background:rgba(10,10,25,.4);opacity:0;visibility:hidden}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(m) {
m.classList.add(m.tagName === 'DIALOG' ? 'msf' : 'msf-inline');
if (m.tagName === 'DIALOG') {
// Esc: animate out instead of vanishing.
m.addEventListener('cancel', function (e) { e.preventDefault(); toggle(m, false); });
m.addEventListener('click', function (e) {
if (e.target !== m) return;
var r = m.getBoundingClientRect();
var inside = e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom;
if (!inside) toggle(m, false);
});
} else {
var bd = document.createElement('div');
bd.className = 'msf-backdrop';
bd.setAttribute('aria-hidden', 'true');
m.parentNode.insertBefore(bd, m);
bd.addEventListener('click', function () { toggle(m, false); });
m._msfBackdrop = bd;
}
m.addEventListener('click', function (e) {
if (e.target.closest('[data-modal-close]')) toggle(m, false);
});
}
function isOpen(m) { return m.tagName === 'DIALOG' ? m.open : m.style.visibility === 'visible'; }
function toggle(m, open) {
if (!m.classList.contains('msf') && !m.classList.contains('msf-inline')) setup(m);
if (open === undefined) open = !isOpen(m);
if (m._msfBusy || open === isOpen(m)) return;
var d = m.dataset;
var duration = reduced() ? 0 : +d.duration || DEFAULTS.duration;
var from = +d.from || DEFAULTS.from;
var bd = m._msfBackdrop;
m._msfBusy = true;
if (open) {
if (m.tagName === 'DIALOG') {
m.classList.remove('msf-out');
m.classList.add('msf-in');
m.showModal();
} else {
m.style.visibility = 'visible';
bd.style.visibility = 'visible';
anime.animate(bd, { opacity: [0, 1], duration: duration * 0.8, ease: 'out(2)' });
}
anime.animate(m, {
opacity: [0, 1], scale: [from, 1], y: [12, 0], duration: duration, ease: 'outBack(1.3)',
onComplete: function () { m._msfBusy = false; }
});
} else {
if (m.tagName === 'DIALOG') { m.classList.remove('msf-in'); m.classList.add('msf-out'); }
else anime.animate(bd, { opacity: 0, duration: duration * 0.7, ease: 'in(2)' });
anime.animate(m, {
opacity: 0, scale: 1 - (1 - from) / 2, y: 6, duration: duration * 0.65, ease: 'in(2)',
onComplete: function () {
if (m.tagName === 'DIALOG') { m.close(); m.classList.remove('msf-out'); }
else { m.style.visibility = 'hidden'; bd.style.visibility = 'hidden'; }
m._msfBusy = false;
}
});
}
}
function init() {
document.querySelectorAll('[data-modal-scale-fade]').forEach(function (m) {
if (m._msf) return;
m._msf = true;
setup(m);
});
document.addEventListener('click', function (e) {
var t = e.target.closest && e.target.closest('[data-modal-open]');
if (!t) return;
var m = document.querySelector(t.getAttribute('data-modal-open'));
if (!m) return;
e.preventDefault();
toggle(m, true);
});
}
window.animacious = window.animacious || {};
window.animacious.modal = function (m, open) { toggle(m, open); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button data-modal-open="#signup">Sign up</button>
<dialog id="signup" data-modal-scale-fade>
<h2>Join us</h2>
<p>…</p>
<button data-modal-close>Close</button>
</dialog>