File Upload Progress
Files upload one under another, each with its own bar and percentage, at uneven speeds like a real connection. As each one finishes its bar turns green and folds away, the file size slides in and a tick pops into a circle.
How to use it
- Paste the snippet just before
</head>. - Add an empty element with
data-file-upload-progress. - For a demo, list pretend files in
data-files. For real uploads, add a row per file from your upload code and report its progress (see below).
Options
Add any of these attributes to the same element.
data-files | Pretend files that upload by themselves, as name:size pairs, comma-separated. |
data-loop | Start the pretend uploads again once they’ve all finished. |
data-speed | Pretend upload speed in MB per second. Default 1.6. |
data-color | Bar colour while uploading. Default #6a0dad. |
var row = animacious.fileUploadProgress.add(el, file.name, file.size) adds a row. Then call row.progress(0.4) (0 to 1) as it goes, row.done() at the end, or row.error('Too big') for a red cross and a shake. The file badge is coloured by its extension.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// File Upload Progress: a list of files uploading, each with its own bar
// and percentage. When a file finishes, its bar turns green and folds
// away, the size slides in and a tick pops into a circle.
// Usage (demo): <div data-file-upload-progress data-files="Brief.pdf:2.4MB, Photo.jpg:3.1MB"></div>
// The files upload at made-up speeds, so you can see the effect.
// Usage (real): <div data-file-upload-progress></div>, then drive it
// from your upload code (see Script use).
// Optional: data-files (pretend files, "name:size" comma-separated),
// data-loop (start the pretend uploads again after they all finish),
// data-color (bar colour, default #6a0dad), data-speed (pretend upload
// speed in MB per second, default 1.6).
// Script use: var f = animacious.fileUploadProgress.add(el, file.name, file.size);
// f.progress(0.4) (0 to 1), f.done(), f.error('Upload failed'), f.remove().
var DEFAULTS = { color: '#6a0dad', speed: 1.6 };
var TICK = '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="#16a34a"/>' +
'<path d="M7 12.5l3.3 3.3L17 9" fill="none" stroke="#fff" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';
var CROSS = '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="#dc2626"/>' +
'<path d="M8.5 8.5l7 7M15.5 8.5l-7 7" stroke="#fff" stroke-width="2.4" stroke-linecap="round"/></svg>';
var KINDS = {
pdf: '#e5484d', doc: '#3b82f6', docx: '#3b82f6', txt: '#71717a', jpg: '#0ea5a4', jpeg: '#0ea5a4', png: '#0ea5a4', gif: '#0ea5a4',
svg: '#f59e0b', mp4: '#8b5cf6', mov: '#8b5cf6', mp3: '#ec4899', zip: '#f59e0b', csv: '#16a34a', xls: '#16a34a', xlsx: '#16a34a', ppt: '#f97316', pptx: '#f97316'
};
var css =
'.fup{display:grid;gap:.6em;margin:0;padding:0;list-style:none;text-align:left}' +
'.fup-item{display:flex;align-items:center;gap:.75em;box-sizing:border-box;padding:.7em .85em;border-radius:.8em;background:#fff;' +
'box-shadow:0 0 0 1px rgba(0,0,0,.07),0 4px 14px rgba(15,15,30,.05);overflow:hidden}' +
'.fup-kind{flex:none;display:grid;place-items:center;width:3.3em;height:3.7em;border-radius:.55em;color:#fff;font-size:.7em;font-weight:700;letter-spacing:0;text-transform:uppercase}' +
'.fup-main{display:block;flex:1;min-width:0}' +
'.fup-row{display:flex;align-items:baseline;gap:.6em}' +
'.fup-name{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:600;color:#27272a}' +
'.fup-pct{flex:none;font-size:.85em;color:#71717a;font-variant-numeric:tabular-nums}' +
'.fup-sub{display:block;position:relative;height:1.35em;margin-top:.3em}' +
'.fup-bar{display:block;position:absolute;left:0;right:0;top:.45em;height:.38em;border-radius:1em;background:#ececf0;overflow:hidden}' +
'.fup-fill{display:block;height:100%;width:100%;border-radius:inherit;background:var(--fup-color);transform-origin:0 50%;transform:scaleX(0);transition:background-color .3s}' +
'.fup-ok .fup-fill{background:#16a34a}.fup-fail .fup-fill{background:#dc2626}' +
'.fup-size{position:absolute;left:0;top:0;font-size:.85em;line-height:1.6;color:#71717a;opacity:0;white-space:nowrap}' +
'.fup-fail .fup-size{color:#dc2626}' +
'.fup-end{flex:none;width:1.5em;height:1.5em;display:grid}' +
'.fup-end svg{grid-area:1/1;width:100%;height:100%;opacity:0}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function on(v) { return v !== undefined && v !== 'false'; }
function size(n) {
if (n < 1024) return n + ' B';
if (n < 1048576) return Math.round(n / 1024) + ' KB';
return (n / 1048576).toFixed(1) + ' MB';
}
function bytes(s) {
var m = /([\d.]+)\s*(b|kb|mb|gb)?/i.exec(s || '');
if (!m) return 1048576;
var u = (m[2] || 'mb').toLowerCase();
return Math.round(parseFloat(m[1]) * (u === 'gb' ? 1073741824 : u === 'mb' ? 1048576 : u === 'kb' ? 1024 : 1));
}
function list(el) {
if (el._fupList) return el._fupList;
el.classList.add('fup');
el.style.setProperty('--fup-color', el.dataset.color || DEFAULTS.color);
if (!el.closest('[aria-hidden="true"]')) { el.setAttribute('role', 'list'); el.setAttribute('aria-live', 'polite'); }
el._fupList = el;
return el;
}
// Adds one file row and returns its controls.
function add(el, name, total) {
list(el);
var ext = (/\.([a-z0-9]+)$/i.exec(name) || [])[1] || 'file';
ext = ext.toLowerCase();
var row = document.createElement('div');
row.className = 'fup-item';
row.setAttribute('role', 'listitem');
row.innerHTML = '<span class="fup-kind" aria-hidden="true"></span><span class="fup-main"><span class="fup-row"><span class="fup-name"></span>' +
'<span class="fup-pct">0%</span></span><span class="fup-sub"><span class="fup-bar"><span class="fup-fill"></span></span><span class="fup-size"></span></span></span>' +
'<span class="fup-end" aria-hidden="true">' + TICK + CROSS + '</span>';
var kind = row.firstChild;
kind.textContent = ext.slice(0, 4);
kind.style.background = KINDS[ext] || '#a1a1aa';
var nameEl = row.querySelector('.fup-name'), pct = row.querySelector('.fup-pct');
var bar = row.querySelector('.fup-bar'), fillEl = row.querySelector('.fup-fill'), sizeEl = row.querySelector('.fup-size');
var tick = row.querySelector('.fup-end').firstChild, cross = row.querySelector('.fup-end').lastChild;
nameEl.textContent = name;
el.appendChild(row);
if (!reduced()) anime.animate(row, { opacity: [0, 1], y: [12, 0], duration: 420, ease: 'out(3)' });
var finished = false;
function end(ok, message) {
if (finished) return;
finished = true;
row.classList.add(ok ? 'fup-ok' : 'fup-fail');
if (ok) anime.utils.set(fillEl, { scaleX: 1 });
pct.textContent = ok ? 'Done' : 'Failed';
sizeEl.textContent = ok ? size(total) : (message || 'Upload failed');
var mark = ok ? tick : cross;
if (reduced()) {
anime.utils.set(bar, { opacity: 0 });
anime.utils.set(sizeEl, { opacity: 1 });
anime.utils.set(mark, { opacity: 1 });
return;
}
anime.animate(bar, { opacity: 0, scaleY: 0.2, duration: 300, delay: 280, ease: 'in(2)' });
anime.animate(sizeEl, { opacity: [0, 1], y: [8, 0], duration: 360, delay: 500, ease: 'out(3)' });
anime.animate(mark, { opacity: [0, 1], scale: [0, 1], rotate: ok ? [-60, 0] : [0, 0], duration: 560, delay: 200, ease: 'outBack(2.6)' });
if (!ok) anime.animate(row, { x: [0, -6, 5, -3, 0], duration: 420, delay: 160, ease: 'out(2)' });
}
return {
row: row,
progress: function (p) {
if (finished) return;
p = Math.max(0, Math.min(1, p));
anime.utils.set(fillEl, { scaleX: p });
pct.textContent = Math.floor(p * 100) + '%';
},
done: function () { end(true); },
error: function (message) { end(false, message); },
remove: function () {
if (reduced()) { row.remove(); return; }
anime.animate(row, { opacity: 0, x: 20, duration: 260, ease: 'in(2)', onComplete: function () { row.remove(); } });
}
};
}
// The pretend uploads: uneven speeds with the odd stall, like a real network.
function demo(el) {
var d = el.dataset;
var files = d.files.split(',').map(function (s) {
var parts = s.trim().split(':');
return { name: parts[0].trim(), bytes: bytes(parts[1]) };
}).filter(function (f) { return f.name; });
var speed = (parseFloat(d.speed) || DEFAULTS.speed) * 1048576;
var items = [], raf = 0, last = 0, visible = false, holdUntil = 0;
function start() {
el.textContent = '';
items = files.map(function (f, i) {
return { f: f, ctl: add(el, f.name, f.bytes), sent: 0, wait: i * 350, rate: 0.6 + Math.random() * 0.8, next: 0, over: false };
});
holdUntil = 0;
}
function frame(now) {
raf = 0;
if (!visible) return;
var dt = Math.min(64, now - (last || now));
last = now;
var busy = false;
items.forEach(function (it) {
if (it.over) return;
busy = true;
if (it.wait > 0) { it.wait -= dt; return; }
if (now > it.next) { it.rate = Math.random() < 0.15 ? 0.08 : 0.5 + Math.random() * 1.1; it.next = now + 250 + Math.random() * 400; }
// Small files still take a moment, big ones don't take forever.
var t = Math.min(3.5, Math.max(1.4, it.f.bytes / speed));
it.sent = Math.min(it.f.bytes, it.sent + it.f.bytes / t * it.rate * dt / 1000);
it.ctl.progress(it.sent / it.f.bytes);
if (it.sent >= it.f.bytes) { it.over = true; it.ctl.done(); }
});
if (!busy) {
if (!on(d.loop)) return;
if (!holdUntil) holdUntil = now + 2600;
if (now > holdUntil) {
holdUntil = 0;
var rows = Array.prototype.slice.call(el.children);
anime.animate(rows, {
opacity: 0, y: -8, duration: 320, delay: anime.stagger(60), ease: 'in(2)',
onComplete: function () { if (visible) { start(); if (!raf) raf = requestAnimationFrame(frame); } }
});
return;
}
}
raf = requestAnimationFrame(frame);
}
list(el);
if (reduced()) {
start();
items.forEach(function (it) { it.ctl.done(); });
return;
}
var begun = false;
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
if (visible && !begun) { begun = true; start(); }
if (visible && !raf) { last = 0; raf = requestAnimationFrame(frame); }
}, { threshold: 0.2 }).observe(el);
}
window.animacious = window.animacious || {};
window.animacious.fileUploadProgress = { add: add };
function init() {
document.querySelectorAll('[data-file-upload-progress]').forEach(function (el) {
if (el._fup) return;
el._fup = true;
list(el);
if (el.dataset.files) demo(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-file-upload-progress id="uploads"></div>
<script>
function upload(file) {
var row = animacious.fileUploadProgress.add(document.getElementById('uploads'), file.name, file.size);
var xhr = new XMLHttpRequest();
xhr.upload.onprogress = function (e) { row.progress(e.loaded / e.total); };
xhr.onload = function () { xhr.status < 300 ? row.done() : row.error('Upload failed'); };
xhr.open('POST', '/upload');
xhr.send(file);
}
</script>