← Loaders

Folding Cube

Four squares make a diamond. One by one they fold in from nothing, flipping over in 3D, hold for a beat, then fold away again in the same order. Size it with font-size and colour it with color.

How to use it

  1. Paste the snippet just before </head>.
  2. Add an empty <div data-folding-cube> where the loader should appear.

Options

Add any of these attributes to the same element.

data-speedTime for one full fold in and out. Default 2400 ms.
data-colorsFour comma-separated colours, one per square, clockwise from the top.
data-gapSpace between the squares, in em. Default 0.06.
data-tilt="false"Sit square instead of as a diamond.

It only moves while it’s on screen. With reduced motion all four squares show.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Folding Cube: four squares make a diamond. One by one they fold in from
    // nothing, flipping over in 3D, hold for a beat, then fold away again in
    // the same order. Sized with font-size, coloured with color.
    // It only moves while it's on screen.
    // Usage: <div data-folding-cube aria-label="Loading"></div>
    // Optional: data-speed (ms for one full fold-in and fold-out, default
    // 2400), data-colors (four comma-separated colours, one per square),
    // data-gap (space between squares, in em, default 0.06), data-tilt="false"
    // (a plain square instead of a diamond).
    var DEFAULTS = { speed: 2400, gap: 0.06 };
    var SIZE = 1.7; // em, the side of the whole square

    var css =
      '.fdc{position:relative;display:inline-block;width:' + SIZE * 1.42 + 'em;height:' + SIZE * 1.42 + 'em}' +
      '.fdc-box{position:absolute;left:50%;top:50%;width:' + SIZE + 'em;height:' + SIZE + 'em;margin:-' + SIZE / 2 + 'em 0 0 -' + SIZE / 2 + 'em;transform:rotate(45deg)}' +
      '.fdc-flat .fdc-box{transform:none}' +
      '.fdc-q{position:absolute;left:0;top:0;width:50%;height:50%;perspective:' + SIZE * 2.4 + 'em}' +
      '.fdc-f{position:absolute;inset:0;background:currentColor;transform-origin:100% 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 build(el) {
      var d = el.dataset;
      var colors = d.colors ? d.colors.split(',') : [];
      var gap = d.gap != null ? Math.max(0, +d.gap) : DEFAULTS.gap;
      el.classList.add('fdc');
      if (d.tilt === 'false') el.classList.add('fdc-flat');
      el.setAttribute('role', 'progressbar');
      if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Loading');
      el.textContent = '';
      var box = document.createElement('span');
      box.className = 'fdc-box';
      box.setAttribute('aria-hidden', 'true');
      el.appendChild(box);
      // Top left, top right, bottom right, bottom left: each quarter is the
      // top-left one turned round the centre, so every fold hinges on the
      // edges that meet in the middle.
      var spots = [[0, 0], [50, 0], [50, 50], [0, 50]];
      var faces = spots.map(function (s, i) {
        var q = document.createElement('span');
        q.className = 'fdc-q';
        q.style.left = s[0] + '%';
        q.style.top = s[1] + '%';
        q.style.transform = 'rotate(' + i * 90 + 'deg)';
        var f = document.createElement('span');
        f.className = 'fdc-f';
        f.style.inset = gap / 2 + 'em';
        if (colors[i]) f.style.background = colors[i].trim();
        q.appendChild(f);
        box.appendChild(q);
        return f;
      });
      if (reduced()) {
        faces.forEach(function (f) { f.style.opacity = 1; });
        return null;
      }
      var D = +d.speed || DEFAULTS.speed;
      return faces.map(function (f, i) {
        anime.utils.set(f, { rotateX: -180, rotateY: 0, opacity: 0 });
        var a = anime.animate(f, {
          // Fold in over the inner edge...
          rotateX: [
            { to: -180, duration: D * 0.1 },
            { to: 0, duration: D * 0.15, ease: 'inOut(2)' },
            { to: 0, duration: D * 0.75 }
          ],
          // ...hold, then fold away over the other inner edge.
          rotateY: [
            { to: 0, duration: D * 0.75 },
            { to: 180, duration: D * 0.15, ease: 'inOut(2)' },
            { to: 180, duration: D * 0.1 }
          ],
          opacity: [
            { to: 0, duration: D * 0.1 },
            { to: 1, duration: D * 0.15, ease: 'out(2)' },
            { to: 1, duration: D * 0.5 },
            { to: 0, duration: D * 0.15, ease: 'in(2)' },
            { to: 0, duration: D * 0.1 }
          ],
          loop: true,
          autoplay: false
        });
        // Each square runs a step behind the one before it.
        a.seek((D - i * D * 0.125) % D);
        return a;
      });
    }

    function init() {
      document.querySelectorAll('[data-folding-cube]').forEach(function (el) {
        if (el._fdc) return;
        el._fdc = 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>
2 · Your HTML
<div data-folding-cube style="font-size: 24px; color: #6a0dad" aria-label="Loading"></div>