← Buttons

Loading to Success

Press the button and it shrinks into a spinning circle, then turns green with a tick that draws itself in, then quietly returns to normal.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-loading-to-success to a button.

Options

Add any of these attributes to the same element.

data-waitHow long it pretends to load. Default 1600 ms. Use manual to wait for your code.
data-holdHow long the tick stays. Default 1600 ms.
data-success-colorBackground when done. Default #16a34a.

For a real request, set data-wait="manual" and call animacious.success(button) when it finishes, or animacious.reset(button) if it fails. The button needs a solid background colour.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Loading to Success: press the button and it shrinks into a spinning
    // circle, then turns green with a tick that draws itself in, then returns.
    // Usage: <button data-loading-to-success>Save changes</button>
    //   On its own it pretends to work for data-wait ms. For a real request, set
    //   data-wait="manual" and call animacious.success(button) when it finishes
    //   (or animacious.reset(button) if it failed).
    // Optional: data-wait (ms, default 1600, or "manual"), data-hold (ms the tick
    // stays, default 1600), data-success-color (default #16a34a).
    // Script use: animacious.loading(button) starts it from code.
    var DEFAULTS = { wait: 1600, hold: 1600, success: '#16a34a' };

    var css =
      '.lts{position:relative;overflow:hidden;white-space:nowrap;box-sizing:border-box}' +
      '.lts-label{display:inline-block}' +
      '.lts-icon{position:absolute;left:50%;top:50%;width:1.3em;height:1.3em;margin:-0.65em 0 0 -0.65em;opacity:0;pointer-events:none;overflow:visible}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function setup(btn) {
      btn.classList.add('lts');
      var label = document.createElement('span');
      label.className = 'lts-label';
      while (btn.firstChild) label.appendChild(btn.firstChild);
      btn.appendChild(label);
      var ns = 'http://www.w3.org/2000/svg';
      var spin = document.createElementNS(ns, 'svg');
      spin.setAttribute('class', 'lts-icon');
      spin.setAttribute('viewBox', '0 0 24 24');
      spin.setAttribute('aria-hidden', 'true');
      spin.innerHTML = '<circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-opacity=".3" stroke-width="3"/>' +
        '<path d="M12 3a9 9 0 0 1 9 9" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>';
      var tick = document.createElementNS(ns, 'svg');
      tick.setAttribute('class', 'lts-icon');
      tick.setAttribute('viewBox', '0 0 24 24');
      tick.setAttribute('aria-hidden', 'true');
      tick.innerHTML = '<path d="M5 12.5l4.5 4.5L19 7.5" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>';
      btn.appendChild(spin);
      btn.appendChild(tick);
      btn._lts = { state: 'idle', label: label, spin: spin, tick: tick, tickPath: anime.svg.createDrawable(tick.querySelector('path')) };
      btn.addEventListener('click', function () { loading(btn); });
    }

    function loading(btn) {
      var s = btn._lts;
      if (!s || s.state !== 'idle') return;
      s.state = 'loading';
      var d = btn.dataset;
      var r = btn.getBoundingClientRect();
      s.width = r.width;
      s.bg = getComputedStyle(btn).backgroundColor;
      var cs = getComputedStyle(btn);
      s.radius = cs.borderRadius;
      s.pl = cs.paddingLeft;
      s.pr = cs.paddingRight;
      btn.setAttribute('aria-busy', 'true');
      btn.style.width = r.width + 'px';
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
        s.label.style.opacity = 0;
        s.spin.style.opacity = 1;
        btn.style.paddingLeft = btn.style.paddingRight = 0;
        btn.style.width = r.height + 'px';
        btn.style.borderRadius = r.height / 2 + 'px';
      } else {
        anime.animate(s.label, { opacity: 0, duration: 150 });
        anime.animate(btn, { width: r.height + 'px', paddingLeft: 0, paddingRight: 0, borderRadius: r.height / 2 + 'px', duration: 450, ease: 'inOut(3)' });
        anime.animate(s.spin, { opacity: [0, 1], duration: 200, delay: 300 });
        s.spinning = anime.animate(s.spin, { rotate: '1turn', duration: 800, ease: 'linear', loop: true });
      }
      if (d.wait !== 'manual') setTimeout(function () { success(btn); }, (+d.wait || DEFAULTS.wait) + 450);
    }

    function success(btn) {
      var s = btn._lts;
      if (!s || s.state !== 'loading') return;
      s.state = 'success';
      var d = btn.dataset;
      btn.removeAttribute('aria-busy');
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      anime.animate(s.spin, { opacity: 0, duration: reduced ? 0 : 150, onComplete: function () { if (s.spinning) s.spinning.pause(); } });
      anime.animate(btn, { backgroundColor: d.successColor || DEFAULTS.success, duration: reduced ? 0 : 300 });
      anime.utils.set(s.tick, { opacity: 1 });
      if (reduced) anime.utils.set(s.tickPath, { draw: '0 1' });
      else {
        anime.animate(s.tickPath, { draw: ['0 0', '0 1'], duration: 450, delay: 120, ease: 'out(3)' });
        anime.animate(btn, { scale: [1, 1.12, 1], duration: 500, delay: 120, ease: 'out(2)' });
      }
      setTimeout(function () { reset(btn); }, +d.hold || DEFAULTS.hold);
    }

    function reset(btn) {
      var s = btn._lts;
      if (!s || s.state === 'idle') return;
      s.state = 'resetting';
      btn.removeAttribute('aria-busy');
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      var dur = reduced ? 0 : 450;
      anime.animate([s.tick, s.spin], { opacity: 0, duration: dur / 3 });
      anime.animate(btn, {
        width: s.width + 'px', paddingLeft: s.pl, paddingRight: s.pr, borderRadius: s.radius, backgroundColor: s.bg, duration: dur, ease: 'inOut(3)',
        onComplete: function () {
          ['width', 'paddingLeft', 'paddingRight', 'borderRadius', 'backgroundColor'].forEach(function (k) { btn.style[k] = ''; });
          if (s.spinning) s.spinning.pause();
          s.state = 'idle';
        }
      });
      anime.animate(s.label, { opacity: 1, duration: dur / 2, delay: dur * 0.6 });
    }

    function init() {
      document.querySelectorAll('[data-loading-to-success]').forEach(function (btn) {
        if (btn._lts) return;
        setup(btn);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.loading = loading;
    window.animacious.success = success;
    window.animacious.reset = reset;
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<button data-loading-to-success>Save changes</button>

<!-- wait for a real request: -->
<button id="save" data-loading-to-success data-wait="manual">Save</button>
<script>
  save.addEventListener("click", async () => {
    await fetch("/save", { method: "POST" });
    animacious.success(save);
  });
</script>