← Loaders

Flipping Square

A square flips over top to bottom, then left to right, again and again, like a tile being turned on a table. Give it two colours to see its front and back, or make it a spinning coin. 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-flipping-square> where the loader should appear.

Options

Add any of these attributes to the same element.

data-speedTime for each flip. Default 1200 ms.
data-colorsTwo comma-separated colours: front and back. Default color, with a slightly darker back.
data-radiusCorner rounding, in em. Default 0.12.
data-shape="circle"A coin instead of a square.

It only moves while it’s on screen. With reduced motion the square sits still.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Flipping Square: a square flips over top to bottom, then left to right,
    // again and again, like a tile being turned on a table. Give it two
    // colours and you see its front and back. Sized with font-size, coloured
    // with color. It only moves while it's on screen.
    // Usage: <div data-flipping-square aria-label="Loading"></div>
    // Optional: data-speed (ms for each flip, default 1200), data-colors (two
    // comma-separated colours: front and back), data-radius (corner rounding,
    // in em, default 0.12), data-shape="circle" (a coin instead of a square).
    var DEFAULTS = { speed: 1200, radius: 0.12 };
    var SIZE = 1.6; // em

    var css =
      '.flp{display:inline-block;width:' + SIZE + 'em;height:' + SIZE + 'em;perspective:' + SIZE * 3 + 'em}' +
      '.flp-sq{position:relative;display:block;width:100%;height:100%;transform-style:preserve-3d}' +
      '.flp-face{position:absolute;inset:0;background:currentColor;backface-visibility:hidden;-webkit-backface-visibility:hidden}' +
      '.flp-back{transform:rotateX(180deg)}';
    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 radius = d.shape === 'circle' ? '50%' : (d.radius != null ? +d.radius : DEFAULTS.radius) + 'em';
      el.classList.add('flp');
      el.setAttribute('role', 'progressbar');
      if (!el.getAttribute('aria-label')) el.setAttribute('aria-label', 'Loading');
      el.textContent = '';
      var sq = document.createElement('span');
      sq.className = 'flp-sq';
      sq.setAttribute('aria-hidden', 'true');
      // The back is turned top over bottom, so it reads the right way up after
      // the first flip. After the second it's the front again, upside down,
      // which a plain square can't show.
      ['flp-face flp-front', 'flp-face flp-back'].forEach(function (cls, i) {
        var f = document.createElement('span');
        f.className = cls;
        f.style.borderRadius = radius;
        var c = colors[i] || colors[0];
        if (c) f.style.background = c.trim();
        if (i && !colors[1]) f.style.filter = 'brightness(.82)';
        sq.appendChild(f);
      });
      el.appendChild(sq);
      if (reduced()) return null;

      var speed = +d.speed || DEFAULTS.speed;
      anime.utils.set(sq, { rotateX: 0, rotateY: 0 });
      // Turn over top to bottom, then left to right. Ending at -180, -180 looks
      // just like 0, 0, so the loop is seamless.
      return [anime.animate(sq, {
        rotateX: [
          { to: -180, duration: speed, ease: 'inOut(3)' },
          { to: -180, duration: speed }
        ],
        rotateY: [
          { to: 0, duration: speed },
          { to: -180, duration: speed, ease: 'inOut(3)' }
        ],
        loop: true,
        autoplay: false
      })];
    }

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