Sync Status Icon
A cloud’s two arrows spin round while it syncs. When the sync is done they shrink away, a tick draws itself in their place, and the cloud turns green with a little squash and bounce. A short label beside it rolls from “Syncing…” to “All synced”.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-sync-status-icon></div>. Itsfont-sizeis the size of the cloud. - Add
data-manualand tell it what’s happening:animacious.syncStatus(el, "syncing")when a sync starts and(el, "done")when it finishes. Without it, it syncs for a moment and finishes by itself.
Options
Add any of these attributes to the same element.
data-syncing-text | Label while syncing. Default “Syncing…”. |
data-done-text | Label when done. Default “All synced”. |
data-label="false" | Just the icon, no label. |
data-color | Colour while syncing. Default #2563eb. |
data-done-color | Colour when done. Default #16a34a. |
data-duration | How long it syncs before the tick, when it runs by itself. Default 2400 ms. |
data-manual | Spin until you call animacious.syncStatus(el, "done"). |
data-loop | Start syncing again after a pause. |
data-hold | How long the tick shows before looping. Default 2200 ms. |
It’s a status region, so screen readers hear the label change. The arrows only spin while on screen. With reduced motion it shows the finished state, or swaps straight between states when you drive it.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Sync Status Icon: a cloud whose two arrows spin round while it syncs.
// When the sync finishes the arrows shrink away, a tick draws itself in
// their place and the cloud turns green with a little bounce. A short
// label beside it says what is going on.
// Usage: <div data-sync-status-icon></div> (size it with font-size: 1em = the cloud)
// Optional: data-syncing-text (default "Syncing…"), data-done-text
// (default "All synced"), data-label="false" (just the icon), data-color
// (cloud while syncing, default #2563eb), data-done-color (default
// #16a34a), data-duration (ms it syncs for before the tick, default 2400),
// data-loop (start syncing again after a pause), data-hold (ms the tick
// shows before looping, default 2200), data-manual (wait for script calls
// instead of finishing by itself).
// Script use: animacious.syncStatus(el, 'syncing') or (el, 'done').
var DEFAULTS = { syncingText: 'Syncing…', doneText: 'All synced', color: '#2563eb', doneColor: '#16a34a', duration: 2400, hold: 2200 };
var uid = 0;
var css =
'.ssi{display:inline-flex;align-items:center;gap:.16em;line-height:1.2;vertical-align:middle}' +
'.ssi svg{flex:none;width:1em;height:1em;overflow:visible}' +
'.ssi-arrows,.ssi-tick{transform-box:view-box;transform-origin:12px 13.6px}' +
'.ssi-cloud{transform-box:view-box;transform-origin:12px 20px}' +
'.ssi-label{position:relative;display:inline-grid;font-size:.32em;font-weight:700;white-space:nowrap;overflow:hidden}' +
'.ssi-label>span{grid-area:1/1}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function opt(el, k) { var v = el.dataset[k]; return v === undefined ? DEFAULTS[k] : v; }
function setup(el) {
var id = 'ssi-ah-' + (++uid);
var c = opt(el, 'color');
el.classList.add('ssi');
el.style.color = c;
el.setAttribute('role', 'status');
el.innerHTML =
'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
'<defs><marker id="' + id + '" viewBox="0 0 6 6" refX="3" refY="3" markerWidth="2.4" markerHeight="2.4" orient="auto">' +
'<path d="M.5 .5L5 3L.5 5.5z" fill="currentColor" stroke="none"/></marker></defs>' +
'<g class="ssi-cloud"><path d="M18 9.6h-1.26A8 8 0 1 0 9 19.6h9a5 5 0 0 0 0-10z" stroke-width="1.9"/></g>' +
'<g class="ssi-arrows" stroke-width="1.5">' +
'<path d="M8.6 13.6a3.4 3.4 0 0 1 5.5-2.67" marker-end="url(#' + id + ')"/>' +
'<path d="M15.4 13.6a3.4 3.4 0 0 1-5.5 2.67" marker-end="url(#' + id + ')"/></g>' +
'<path class="ssi-tick" d="M8.7 13.9l2.3 2.3l4.4-4.6" stroke-width="2"/>' +
'</svg>' +
(el.dataset.label === 'false' ? '' : '<span class="ssi-label"><span></span><span></span></span>');
var s = el._ssi = {
svg: el.querySelector('svg'), cloud: el.querySelector('.ssi-cloud'),
arrows: el.querySelector('.ssi-arrows'),
tick: anime.svg.createDrawable(el.querySelector('.ssi-tick')),
label: el.querySelector('.ssi-label'), spin: null, timer: null, state: '', visible: false, waiting: false
};
if (s.label) {
s.label.children[0].textContent = opt(el, 'syncingText');
s.label.children[1].textContent = opt(el, 'doneText');
}
if (reduced()) { show(el, el.dataset.manual === undefined ? 'done' : 'syncing', true); return; }
// Hidden until it scrolls into view and starts.
anime.utils.set(s.arrows, { scale: 0, opacity: 0 });
anime.utils.set(s.tick, { draw: '0 0' });
if (s.label) anime.utils.set(s.label.children, { opacity: 0 });
}
function labels(el, done, instant) {
var s = el._ssi;
if (!s.label) return;
var a = s.label.children[done ? 1 : 0], b = s.label.children[done ? 0 : 1];
a.removeAttribute('aria-hidden');
b.setAttribute('aria-hidden', 'true');
if (instant) {
anime.utils.set(a, { opacity: 1, y: '0em' });
anime.utils.set(b, { opacity: 0, y: '0em' });
return;
}
anime.animate(b, { opacity: 0, y: '-.7em', duration: 260, ease: 'in(2)' });
anime.animate(a, { opacity: [0, 1], y: ['.7em', '0em'], duration: 420, delay: 120, ease: 'out(3)' });
}
function show(el, state, instant) {
var s = el._ssi;
if (s.state === state) return;
s.state = state;
var done = state === 'done';
var c = opt(el, done ? 'doneColor' : 'color');
labels(el, done, instant);
if (instant || reduced()) {
if (s.spin) { s.spin.pause(); s.spin = null; }
el.style.color = c;
anime.utils.set(s.arrows, { rotate: 0, scale: done ? 0 : 1, opacity: done ? 0 : 1 });
anime.utils.set(s.tick, { draw: done ? '0 1' : '0 0' });
return;
}
anime.animate(el, { color: c, duration: 450, ease: 'out(2)' });
if (done) {
// Let the arrows finish their turn and shrink away, then draw the tick.
if (s.spin) { s.spin.pause(); s.spin = null; }
anime.animate(s.arrows, { rotate: '+=200', scale: 0, opacity: 0, duration: 420, ease: 'in(2)' });
anime.animate(s.tick, { draw: ['0 0', '0 1'], duration: 380, delay: 300, ease: 'out(3)' });
anime.animate(s.cloud, {
scaleY: [{ to: 0.86, duration: 140, ease: 'out(2)' }, { to: 1, duration: 650, ease: 'outElastic(1, .45)' }],
scaleX: [{ to: 1.08, duration: 140, ease: 'out(2)' }, { to: 1, duration: 650, ease: 'outElastic(1, .45)' }],
delay: 380
});
} else {
anime.animate(s.tick, { draw: '1 1', duration: 260, ease: 'in(2)' });
anime.utils.set(s.arrows, { rotate: 0 });
anime.animate(s.arrows, { scale: [0, 1], opacity: [0, 1], duration: 420, delay: 160, ease: 'outBack(2)' });
s.spin = anime.animate(s.arrows, { rotate: [0, 360], duration: 1100, loop: true, ease: 'linear', autoplay: s.visible });
}
}
function cycle(el) {
var s = el._ssi;
clearTimeout(s.timer);
if (!s.visible) { s.waiting = true; return; }
s.waiting = false;
show(el, 'syncing');
if (s.spin) s.spin.play();
s.timer = setTimeout(function () {
show(el, 'done');
if (el.dataset.loop === undefined || el.dataset.loop === 'false') return;
s.timer = setTimeout(function () { cycle(el); }, +opt(el, 'hold') + 600);
}, +opt(el, 'duration'));
}
function init() {
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
var s = e.target._ssi;
s.visible = e.isIntersecting;
if (s.spin) { if (s.visible) s.spin.play(); else s.spin.pause(); }
if (!s.visible) return;
if (!s.started) { s.started = true; if (e.target.dataset.manual === undefined) cycle(e.target); else if (!s.state) show(e.target, 'syncing'); }
else if (s.waiting) cycle(e.target);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-sync-status-icon]').forEach(function (el) {
if (el._ssi) return;
setup(el);
if (!reduced()) io.observe(el);
});
}
window.animacious = window.animacious || {};
window.animacious.syncStatus = function (el, state) {
var s = el._ssi;
if (!s) return;
clearTimeout(s.timer);
show(el, state === 'done' ? 'done' : 'syncing');
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-sync-status-icon style="font-size: 48px"></div>
<!-- driven by your own code: -->
<div id="sync" data-sync-status-icon data-manual data-done-text="Backed up"></div>
<script>
const sync = document.getElementById("sync");
saveEverything().then(() => animacious.syncStatus(sync, "done"));
</script>