← Cards & images

Logo Wall Swap

A grid of logos where, every couple of seconds, one tile tips over and comes back with a different logo on it. A small wall can show a long list of customers, and it never flips the same tile twice in a row.

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-logo-wall-swap to a box holding your logos.
  3. Give it more logos than tiles: the spare ones wait their turn to be flipped in.

Options

Add any of these attributes to the same element.

data-columnsHow many tiles across. Default 4. Or set --lws-cols in CSS, say in a media query for phones.
data-visibleHow many tiles to show. Default all the logos but three.
data-intervalTime between swaps. Default 1800 ms.
data-durationTime for one flip. Default 800 ms.
data-axisx tips forwards; y turns sideways. Default x.

It only swaps while on screen, and with reduced motion it just shows the first set.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Logo Wall Swap: a grid of logos where, every so often, one tile flips
    // over and comes back showing a different logo, so a small wall can show
    // off a long list of names. Never the same tile twice in a row.
    // Usage: <div data-logo-wall-swap data-columns="4">
    //          <img src="acme.svg" alt="Acme"> <img src="globex.svg" alt="Globex"> …
    //        </div>
    // Give it more logos than tiles: the spare ones wait their turn.
    // Optional: data-columns (default 4; or set --lws-cols in CSS, e.g. in a
    // media query), data-visible (how many tiles, default all the logos but
    // three), data-interval (ms between swaps, default 1800), data-duration
    // (ms for one flip, default 800), data-axis ("x", default, tips forwards;
    // "y" turns sideways).
    var DEFAULTS = { columns: 4, interval: 1800, duration: 800 };

    var css =
      '.lws{display:grid;grid-template-columns:repeat(var(--lws-cols,4),minmax(0,1fr))}' +
      '.lws-cell{display:grid;perspective:600px;min-width:0}' +
      '.lws-in{display:grid;place-items:center;min-width:0;transform-style:preserve-3d;-webkit-backface-visibility:hidden;backface-visibility:hidden}' +
      '.lws-in>*{max-width:100%}';
    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 logos = Array.prototype.slice.call(el.children);
      if (logos.length < 2) return;
      el.classList.add('lws');
      if (d.columns) el.style.setProperty('--lws-cols', d.columns);
      var visible = Math.max(1, Math.min(logos.length - 1, parseInt(d.visible, 10) || logos.length - 3));
      var cells = [];
      logos.forEach(function (logo, i) {
        if (i >= visible) { el.removeChild(logo); return; }
        var cell = document.createElement('div');
        cell.className = 'lws-cell';
        var inner = document.createElement('div');
        inner.className = 'lws-in';
        el.insertBefore(cell, logo);
        inner.appendChild(logo);
        cell.appendChild(inner);
        cells.push({ inner: inner, logo: logo });
      });
      var s = el._lwsS = { cells: cells, pool: logos.slice(visible), last: -1, timer: 0, visible: false, busy: false };
      if (reduced()) return;

      var interval = +d.interval || DEFAULTS.interval;
      function tick() {
        if (s.visible && !document.hidden) swap(el);
        s.timer = setTimeout(tick, interval);
      }
      new IntersectionObserver(function (en) {
        s.visible = en[0].isIntersecting;
        if (s.visible && !s.timer) s.timer = setTimeout(tick, interval * 0.6);
      }, { threshold: 0.2 }).observe(el);
    }

    function swap(el) {
      var s = el._lwsS, d = el.dataset;
      if (!s.pool.length) return;
      var i;
      do { i = Math.floor(Math.random() * s.cells.length); } while (s.cells.length > 1 && i === s.last);
      s.last = i;
      var c = s.cells[i];
      if (c.busy) return;
      c.busy = true;
      var dur = +d.duration || DEFAULTS.duration;
      var prop = d.axis === 'y' ? 'rotateY' : 'rotateX';
      var out = {}, back = {};
      out[prop] = [0, 90];
      back[prop] = [-90, 0];
      var tl = anime.createTimeline();
      tl.add(c.inner, Object.assign(out, { opacity: [1, 0.4], duration: dur * 0.42, ease: 'in(2)' }))
        .call(function () {
          var next = s.pool.shift();
          s.pool.push(c.logo);
          c.inner.replaceChild(next, c.logo);
          c.logo = next;
        })
        .add(c.inner, Object.assign(back, { opacity: [0.4, 1], duration: dur * 0.58, ease: 'outBack(1.6)' }))
        .call(function () { c.busy = false; });
    }

    function init() {
      document.querySelectorAll('[data-logo-wall-swap]').forEach(function (el) {
        if (el._lws) return;
        el._lws = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-logo-wall-swap data-columns="4" data-visible="8">
  <img src="logos/acme.svg" alt="Acme">
  <img src="logos/globex.svg" alt="Globex">
  <!-- … ten or more logos -->
</div>