← Overlays

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.

Nice and smooth

It grew in from slightly small. Press Esc, click outside or use the button to close it.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-modal-scale-fade to a <dialog> with an id.
  3. Add data-modal-open="#that-id" to the button that opens it, and data-modal-close to any button inside that closes it.

Options

Add any of these attributes to the same element.

data-durationTime to open. Default 380 ms.
data-fromStarting 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.

1 · Snippet
<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>
2 · Your HTML
<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>