Download Progress Button
Press download and the arrow dips, then drops into its tray as the label slips away. The button turns into a striped progress bar that fills up in fits and starts, its percentage changing colour as the fill passes under it, and it finishes green with a tick before turning back into a button.
How to use it
- Paste the snippet just before
</head>. - Add
data-download-progress-buttonto a button with a solid background. The script adds the arrow icon. - Start the download when it fires its
downloadevent.
Options
Add any of these attributes to the same element.
data-color | Colour of the fill. Default #6a0dad. |
data-track | Colour of the empty bar. Default a pale tint of the fill. |
data-done | Label when finished. Default “Done”. |
data-done-color | Colour when finished. Default #16a34a. |
data-duration | How long the pretend download takes. Default 2600 ms. |
data-hold | How long the tick stays. Default 1600 ms. |
data-manual | Report real progress yourself (see below) instead of the pretend download. |
data-autoplay | It downloads by itself every few seconds while in view, without firing the event. Handy for demos. |
With data-manual, call animacious.downloadProgress(button, p) with p from 0 to 1 as your download goes; at 1 it shows the tick. While it runs the button is a progressbar for screen readers.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Download Progress Button: press it and the arrow drops into its tray,
// the button turns into a progress bar that fills up with a percentage,
// and it finishes with a tick before turning back into a button.
// Usage: <button data-download-progress-button>Download</button>
// Optional: data-color (colour of the fill, default #6a0dad), data-track
// (colour of the empty bar, default a pale tint of the fill), data-done
// (label when finished, default "Done"), data-done-color (colour when
// finished, default #16a34a), data-duration (ms the pretend download
// takes, default 2600), data-hold (ms the finished state stays, default
// 1600), data-manual (you report real progress instead, see below),
// data-autoplay (it downloads by itself every few seconds while in view,
// without firing the event; handy for demos).
// Events: fires "download" on the button when pressed.
// Script use: with data-manual, call animacious.downloadProgress(button, p)
// with p from 0 to 1 as your download goes; at 1 it shows the tick.
var DEFAULTS = { color: '#6a0dad', done: 'Done', doneColor: '#16a34a', duration: 2600, hold: 1600, every: 2600 };
var ARROW = '<svg class="dpb-icon" viewBox="0 0 24 24" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round">' +
'<g class="dpb-arrow"><path d="M12 3.5v11"/><path d="M7.5 10.5 12 15l4.5-4.5"/></g><path d="M4.5 19.5h15"/></svg>';
var TICK = '<svg class="dpb-icon" viewBox="0 0 24 24" aria-hidden="true"><path fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" pathLength="1" d="M5 12.5l4.5 4.5L19 7.5"/></svg>';
var css =
'.dpb{position:relative;isolation:isolate;overflow:hidden}' +
'.dpb-track,.dpb-fill{position:absolute;inset:0;z-index:-1;border-radius:inherit;pointer-events:none;opacity:0}' +
'.dpb-fill{clip-path:inset(0 100% 0 0);background-image:repeating-linear-gradient(-45deg,rgba(255,255,255,.16) 0 8px,transparent 8px 16px);' +
'background-size:22.6px 22.6px;animation:dpb-stripes .6s linear infinite}' +
'@keyframes dpb-stripes{to{background-position:22.6px 0}}' +
'.dpb-label,.dpb-done{display:inline-flex;align-items:center;gap:.5em}' +
'.dpb-pct,.dpb-done{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;gap:.45em;pointer-events:none;opacity:0;font-variant-numeric:tabular-nums}' +
'.dpb-fill .dpb-pct{color:#fff}' +
'.dpb-done{color:#fff}' +
'.dpb-icon{width:1.15em;height:1.15em;flex:none;overflow:visible}' +
'.dpb-done path{stroke-dasharray:1 1;stroke-dashoffset:1}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function setup(el) {
var d = el.dataset;
var color = d.color || DEFAULTS.color;
var doneColor = d.doneColor || DEFAULTS.doneColor;
var duration = +d.duration || DEFAULTS.duration;
var hold = +d.hold || DEFAULTS.hold;
var manual = d.manual !== undefined && d.manual !== 'false';
el.classList.add('dpb');
var label = document.createElement('span');
label.className = 'dpb-label';
while (el.firstChild) label.appendChild(el.firstChild);
label.insertAdjacentHTML('afterbegin', ARROW);
var arrow = label.querySelector('.dpb-arrow');
var track = document.createElement('span');
track.className = 'dpb-track';
track.style.background = d.track || 'color-mix(in srgb,' + color + ' 16%,#fff)';
var pct = document.createElement('span');
pct.className = 'dpb-pct';
pct.style.color = color;
pct.setAttribute('aria-hidden', 'true');
var fill = document.createElement('span');
fill.className = 'dpb-fill';
fill.style.backgroundColor = color;
var pctIn = pct.cloneNode();
pctIn.style.color = '';
pctIn.style.opacity = 1;
fill.appendChild(pctIn);
var done = document.createElement('span');
done.className = 'dpb-done';
done.setAttribute('aria-hidden', 'true');
done.innerHTML = TICK + '<span></span>';
done.lastChild.textContent = d.done || DEFAULTS.done;
var tick = done.querySelector('path');
[track, fill, label, pct, done].forEach(function (n) { el.appendChild(n); });
var state = 'idle', shown = 0, prog = { p: 0 }, still = reduced();
function draw(p) {
fill.style.clipPath = 'inset(0 ' + (100 - p * 100) + '% 0 0)';
var text = Math.round(p * 100) + '%';
if (pct.textContent !== text) {
pct.textContent = pctIn.textContent = text;
el.setAttribute('aria-valuenow', Math.round(p * 100));
}
}
function begin() {
if (state !== 'idle') return false;
state = 'loading';
el.style.width = el.offsetWidth + 'px';
el.setAttribute('role', 'progressbar');
el.setAttribute('aria-valuemin', '0');
el.setAttribute('aria-valuemax', '100');
label.setAttribute('aria-hidden', 'true');
shown = 0;
prog.p = 0;
draw(0);
var t = still ? 0 : 1;
// The arrow dips, then drops into the tray as the label slips away.
anime.animate(arrow, { y: [0, -3, 16], opacity: [1, 1, 0], duration: 420 * t, ease: 'in(2)' });
anime.animate(label, { opacity: 0, y: 6, duration: 220 * t, delay: 260 * t, ease: 'in(2)' });
anime.animate([track, fill], { opacity: 1, duration: 260 * t, delay: 360 * t, ease: 'out(2)' });
anime.animate(pct, { opacity: [0, 1], scale: [0.8, 1], duration: 260 * t, delay: 420 * t, ease: 'out(3)' });
anime.animate(el, { scaleY: [1, 0.9, 1], duration: 500 * t, delay: 320 * t, ease: 'out(3)' });
return true;
}
function setProgress(p) {
if (state !== 'loading') return;
p = Math.max(shown, Math.min(1, p));
shown = p;
anime.animate(prog, {
p: p, duration: still ? 0 : 320, ease: 'out(2)',
onUpdate: function () { draw(prog.p); },
onComplete: function () { if (p >= 1 && state === 'loading') finish(); }
});
}
function simulate() {
// A believable download: quick bursts and short stalls.
var steps = [0.18, 0.31, 0.58, 0.66, 0.89, 1];
var tl = anime.createTimeline({ delay: still ? 0 : 520, onComplete: finish });
steps.forEach(function (p, i) {
tl.add(prog, {
p: p, duration: still ? 0 : duration / steps.length * (i % 2 ? 0.7 : 1.3), ease: 'inOut(2)',
onUpdate: function () { draw(prog.p); }
}, i ? '+=' + (still ? 0 : 60 + (i % 3) * 90) : 0);
});
}
function finish() {
if (state !== 'loading') return;
state = 'done';
draw(1);
var t = still ? 0 : 1;
anime.animate(pct, { opacity: 0, duration: 160 * t, ease: 'in(2)' });
anime.animate(pctIn, { opacity: 0, duration: 160 * t, ease: 'in(2)' });
anime.animate(fill, { backgroundColor: doneColor, duration: 300 * t, ease: 'out(2)' });
anime.animate(el, { scale: [1, 1.06, 1], duration: 520 * t, delay: 120 * t, ease: 'outBack(2)' });
anime.animate(done, { opacity: [0, 1], y: [8, 0], duration: 320 * t, delay: 140 * t, ease: 'out(3)' });
anime.animate(tick, { strokeDashoffset: [1, 0], duration: 420 * t, delay: 260 * t, ease: 'out(2)' });
done.removeAttribute('aria-hidden');
el.setAttribute('aria-valuenow', '100');
setTimeout(reset, hold + 300);
}
function reset() {
var t = still ? 0 : 1;
anime.animate(done, { opacity: 0, y: -6, duration: 220 * t, ease: 'in(2)' });
anime.animate([track, fill], {
opacity: 0, duration: 320 * t, delay: 120 * t, ease: 'inOut(2)',
onComplete: function () {
fill.style.backgroundColor = color;
pctIn.style.opacity = 1;
anime.utils.set(tick, { strokeDashoffset: 1 });
}
});
anime.animate(label, { opacity: [0, 1], y: [8, 0], duration: 320 * t, delay: 260 * t, ease: 'out(3)' });
anime.animate(arrow, {
y: [-10, 0], opacity: [0, 1], duration: 420 * t, delay: 320 * t, ease: 'outBack(2)',
onComplete: function () {
['role', 'aria-valuemin', 'aria-valuemax', 'aria-valuenow'].forEach(function (a) { el.removeAttribute(a); });
label.removeAttribute('aria-hidden');
done.setAttribute('aria-hidden', 'true');
el.style.width = '';
state = 'idle';
}
});
}
el._dpbProgress = function (p) {
if (state === 'idle') begin();
setProgress(p);
};
function start(fire) {
if (state !== 'idle') return;
if (fire) el.dispatchEvent(new CustomEvent('download', { bubbles: true }));
if (begin() && !(manual && fire)) simulate();
}
el.addEventListener('click', function () { start(true); });
if (!still && d.autoplay !== undefined && d.autoplay !== 'false') {
var timer = null;
var loop = function () {
if (state === 'idle' && !el.matches(':hover')) start(false);
timer = setTimeout(loop, duration + hold + DEFAULTS.every);
};
new IntersectionObserver(function (entries) {
clearTimeout(timer);
if (entries[0].isIntersecting) timer = setTimeout(loop, 700);
}).observe(el);
}
}
window.animacious = window.animacious || {};
window.animacious.downloadProgress = function (el, p) { if (el && el._dpbProgress) el._dpbProgress(p); };
function init() {
document.querySelectorAll('[data-download-progress-button]').forEach(function (el) {
if (el._dpb) return;
el._dpb = true;
setup(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><button id="dl" data-download-progress-button>Download</button>
<!-- reporting real progress: -->
<button id="dl" data-download-progress-button data-manual>Download</button>
<script>
dl.addEventListener("download", async () => {
// … as data arrives:
animacious.downloadProgress(dl, received / total);
});
</script>