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
- Paste the snippet just before
</head>. - Add
data-loading-to-successto a button.
Options
Add any of these attributes to the same element.
data-wait | How long it pretends to load. Default 1600 ms. Use manual to wait for your code. |
data-hold | How long the tick stays. Default 1600 ms. |
data-success-color | Background 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.
<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><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>