Wandering Cubes
Two cubes start at opposite corners and travel round a square path, turning a quarter turn along every side, shrinking at one corner and growing back at the next. Size it with font-size and colour it with color.
How to use it
- Paste the snippet just before
</head>. - Add an empty
<div data-wandering-cubes>where the loader should appear.
Options
Add any of these attributes to the same element.
data-speed | Time for one lap. Default 1800 ms. |
data-cubes | 1 or 2 cubes. Default 2. |
data-colors | Two comma-separated colours, one per cube. Default color, the second one faded. |
data-shrink | How small the cubes get at the in-between corners. Default 0.5. |
It only moves while it’s on screen. With reduced motion the cubes sit still in their corners.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Wandering Cubes: two cubes start at opposite corners and travel round a
// square path, a quarter turn on every side, shrinking to half size at
// every other corner and growing back at the next. Sized with font-size,
// coloured with color. It only moves while it's on screen.
// Usage: <div data-wandering-cubes aria-label="Loading"></div>
// Optional: data-speed (ms for one lap, default 1800), data-cubes (1 or 2,
// default 2), data-colors (two comma-separated colours, one per cube),
// data-shrink (size at the small corners, default 0.5).
var DEFAULTS = { speed: 1800, cubes: 2, shrink: 0.5 };
var PATH = 1.5, CUBE = 0.55; // em
var css =
'.wnc{position:relative;display:inline-block;width:' + (PATH + CUBE) + 'em;height:' + (PATH + CUBE) + 'em}' +
'.wnc-c{position:absolute;left:0;top:0;width:' + CUBE + 'em;height:' + CUBE + 'em;border-radius:0.06em;background:currentColor}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }
// The four corners, clockwise from top left.
var CORNERS = [[0, 0], [PATH, 0], [PATH, PATH], [0, PATH]];
function build(el) {
var d = el.dataset;
var n = +d.cubes === 1 ? 1 : DEFAULTS.cubes;
var shrink = Math.max(0.1, Math.min(1, d.shrink != null ? +d.shrink : DEFAULTS.shrink));
var colors = d.colors ? d.colors.split(',') : [];
el.classList.add('wnc');
el.setAttribute('role', 'progressbar');
if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Loading');
el.textContent = '';
var cubes = [];
for (var i = 0; i < n; i++) {
var c = document.createElement('span');
c.className = 'wnc-c';
if (colors[i]) c.style.background = colors[i].trim();
else if (i) c.style.opacity = 0.55;
c.setAttribute('aria-hidden', 'true');
el.appendChild(c);
// The second cube starts at the opposite corner.
var start = CORNERS[i * 2];
anime.utils.set(c, { x: start[0] + 'em', y: start[1] + 'em', rotate: 0, scale: 1 });
cubes.push(c);
}
if (reduced()) return null;
var leg = (+d.speed || DEFAULTS.speed) / 4;
var ease = 'inOut(2)';
return cubes.map(function (c, i) {
var x = [], y = [], rotate = [], scale = [];
for (var k = 1; k <= 4; k++) {
var p = CORNERS[(i * 2 + k) % 4];
x.push({ to: p[0] + 'em', duration: leg, ease: ease });
y.push({ to: p[1] + 'em', duration: leg, ease: ease });
// A quarter turn per side, anticlockwise against the clockwise path.
rotate.push({ to: -90 * k, duration: leg, ease: ease });
scale.push({ to: k % 2 ? shrink : 1, duration: leg, ease: ease });
}
return anime.animate(c, { x: x, y: y, rotate: rotate, scale: scale, loop: true, autoplay: false });
});
}
function init() {
document.querySelectorAll('[data-wandering-cubes]').forEach(function (el) {
if (el._wnc) return;
el._wnc = true;
var anims = build(el);
if (!anims) return;
new IntersectionObserver(function (entries) {
anims.forEach(function (a) { entries[0].isIntersecting ? a.play() : a.pause(); });
}).observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><div data-wandering-cubes style="font-size: 24px; color: #6a0dad" aria-label="Loading"></div>