Step Checklist Loader
A list of steps ticks off one by one while something loads. The current step gets a spinning ring; when it’s done the ring fills, a tick draws itself in, the line down to the next step fills and the next one starts. Waiting steps sit faded. Let it run by itself, or move it on from your code as each real step finishes.
Getting things ready
- Creating your workspace
- Importing 128 contacts
- Setting up your inbox
- Saying hello
How to use it
- Paste the snippet just before
</head>. - Add
data-step-checklist-loaderto a<ul>with one<li>per step. - To follow real progress, add
data-auto="false"and callanimacious.stepChecklistLoader(list, i)as each step starts.
Options
Add any of these attributes to the same element.
data-speed | Roughly how long each step takes. Steps vary a little so it feels real. Default 1100 ms. |
data-color | Colour of the rings, ticks and line. Default color. |
data-loop | When every step is done, hold, clear and start again. |
data-hold | How long to hold the finished list before looping. Default 1600 ms. |
data-auto="false" | Don’t move on by itself; drive it from your code. |
The step in progress gets aria-current="step". animacious.stepChecklistLoader(list, i) marks steps before i as done and step i as in progress; pass the number of steps to finish them all. It only moves on while it’s on screen. With reduced motion every step shows as done straight away.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Step Checklist Loader: a list of steps ticks off one by one while
// something loads. The current step gets a spinning ring, then the ring
// fills, a tick draws itself in, the line down to the next step fills and
// the next one starts. Steps that are waiting sit faded.
// Usage: <ul data-step-checklist-loader>
// <li>Uploading files</li><li>Optimising images</li><li>Publishing</li>
// </ul>
// Optional: data-speed (ms per step, default 1100; steps vary a little so it
// feels real), data-loop (when all are done, hold, clear and start again),
// data-hold (ms to hold before looping, default 1600), data-color (colour
// of the rings and ticks, default color), data-auto="false" (don't move on
// by itself: drive it from your code instead).
// Script use: animacious.stepChecklistLoader(el, i) marks steps before i as
// done and step i as in progress (i = number of steps: all done).
var DEFAULTS = { speed: 1100, hold: 1600 };
var VARY = [1, 1.35, 0.8, 1.15, 0.9, 1.25];
var css =
'.scl{list-style:none;margin:0;padding:0;display:grid;gap:.55em;text-align:left}' +
'.scl>li{position:relative;display:flex;align-items:flex-start;gap:.65em;margin:0;padding:0;min-height:1.3em;transition:opacity .4s}' +
'.scl-icon{position:relative;flex:none;width:1.3em;height:1.3em;color:var(--scl-c,currentColor)}' +
'.scl-icon svg{display:block;width:100%;height:100%;overflow:visible}' +
'.scl-track{fill:none;stroke:currentColor;stroke-width:2;opacity:.28}' +
'.scl-arc{fill:none;stroke:currentColor;stroke-width:2.4;stroke-linecap:round;stroke-dasharray:18 100;opacity:0;transform-origin:12px 12px;transition:opacity .2s}' +
'.scl-dot{fill:currentColor;transform-origin:12px 12px;transform:scale(0);transition:transform .38s cubic-bezier(.34,1.7,.6,1)}' +
'.scl-tick{fill:none;stroke:#fff;stroke-width:2.6;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:1;stroke-dashoffset:1;transition:stroke-dashoffset .3s .16s ease-out}' +
'.scl-text{line-height:1.3em;opacity:.45;transition:opacity .3s,color .3s}' +
'.scl-line{position:absolute;left:.62em;top:1.42em;width:.06em;height:calc(100% - 1.54em + .55em);background:currentColor;opacity:.14}' +
'.scl-fill{position:absolute;left:.62em;top:1.42em;width:.06em;height:calc(100% - 1.54em + .55em);background:var(--scl-c,currentColor);transform:scaleY(0);transform-origin:50% 0;transition:transform .35s ease-in-out}' +
'.scl>li:last-child .scl-line,.scl>li:last-child .scl-fill{display:none}' +
'.scl-active .scl-arc{opacity:1;animation:scl-spin .8s linear infinite}' +
'.scl-active .scl-text{opacity:1;font-weight:600}' +
'.scl-done .scl-dot{transform:scale(1)}' +
'.scl-done .scl-tick{stroke-dashoffset:0}' +
'.scl-done .scl-text{opacity:.8}' +
'.scl-done .scl-fill{transform:scaleY(1)}' +
'.scl-clear>li{opacity:0}' +
'.scl-still *{transition:none!important;animation:none!important}' +
'@keyframes scl-spin{to{transform:rotate(360deg)}}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
var ICON = '<svg viewBox="0 0 24 24" aria-hidden="true">' +
'<circle class="scl-track" cx="12" cy="12" r="10"/>' +
'<circle class="scl-arc" cx="12" cy="12" r="10" pathLength="100"/>' +
'<circle class="scl-dot" cx="12" cy="12" r="11"/>' +
'<path class="scl-tick" d="M7.5 12.4l3 3 6-6.4" pathLength="1"/></svg>';
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
function build(el) {
var d = el.dataset;
el.classList.add('scl');
if (d.color) el.style.setProperty('--scl-c', d.color);
var items = Array.prototype.slice.call(el.children);
items.forEach(function (li) {
var text = document.createElement('span');
text.className = 'scl-text';
while (li.firstChild) text.appendChild(li.firstChild);
var icon = document.createElement('span');
icon.className = 'scl-icon';
icon.innerHTML = ICON;
li.appendChild(icon);
li.appendChild(text);
['scl-line', 'scl-fill'].forEach(function (c) {
var s = document.createElement('span');
s.className = c;
s.setAttribute('aria-hidden', 'true');
li.appendChild(s);
});
});
function set(i) {
el._sclAt = i;
items.forEach(function (li, k) {
li.classList.toggle('scl-done', k < i);
li.classList.toggle('scl-active', k === i);
if (k === i) li.setAttribute('aria-current', 'step');
else li.removeAttribute('aria-current');
});
}
el._sclSet = set;
if (reduced()) {
el.classList.add('scl-still');
set(d.auto === 'false' ? 0 : items.length);
return;
}
set(0);
if (d.auto === 'false') return;
var speed = Math.max(200, +d.speed || DEFAULTS.speed);
var hold = Math.max(0, d.hold != null && !isNaN(+d.hold) ? +d.hold : DEFAULTS.hold);
var loop = d.loop !== undefined && d.loop !== 'false';
var visible = false, running = false, timer = 0, due = 0, left = 0, next = null;
// A pausable timeout: it only counts down while the list is on screen.
function after(ms, fn) { next = fn; left = ms; if (visible) start(); }
function start() {
if (running || !next) return;
running = true;
due = performance.now() + left;
timer = setTimeout(function () { running = false; var f = next; next = null; f(); }, left);
}
function stop() {
if (!running) return;
running = false;
clearTimeout(timer);
left = Math.max(0, due - performance.now());
}
function step() {
var i = el._sclAt;
if (i < items.length) {
set(i + 1);
if (i + 1 < items.length) after(speed * VARY[(i + 1) % VARY.length], step);
else if (loop) after(hold, clear);
}
}
function clear() {
el.classList.add('scl-clear');
after(450, function () {
el.classList.add('scl-still');
set(0);
el.offsetWidth; // apply the reset without transitions
el.classList.remove('scl-still', 'scl-clear');
after(speed * VARY[0], step);
});
}
after(speed * VARY[0], step);
new IntersectionObserver(function (entries) {
visible = entries[0].isIntersecting;
visible ? start() : stop();
}).observe(el);
}
window.animacious = window.animacious || {};
window.animacious.stepChecklistLoader = function (el, i) {
if (el && el._sclSet) el._sclSet(Math.max(0, Math.round(+i) || 0));
};
function init() {
document.querySelectorAll('[data-step-checklist-loader]').forEach(function (el) {
if (el._scl) return;
el._scl = true;
build(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><ul data-step-checklist-loader data-color="#6a0dad">
<li>Uploading files</li>
<li>Optimising images</li>
<li>Publishing</li>
</ul>
<!-- driven from your code: -->
<ul id="deploy" data-step-checklist-loader data-auto="false">…</ul>
<script>
animacious.stepChecklistLoader(document.getElementById("deploy"), 1);
</script>