← Backgrounds

Infinite Marquee

A row of logos, words or cards glides sideways forever, with no gap or jump where it wraps round. It eases to a stop when the pointer is over it.

CopyPasteShipFastSimpleFreeYours
CopyPasteShipFastSimpleFreeYours

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-infinite-marquee to a container holding the items: logos, words, cards.

Options

Add any of these attributes to the same element.

data-speedPixels per second. Default 60.
data-direction="right"Move to the right instead of the left.
data-gapSpace between items. Default 2.5em.
data-fade="false"Don’t fade out the edges.
data-pause-on-hover="false"Keep moving when the pointer is over it.

The items are copied as often as needed to fill the row; copies are hidden from screen readers.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Infinite Marquee: a row of logos, words or cards glides sideways forever,
    // with no gap or jump where it wraps round. It eases to a stop on hover.
    // Usage:
    //   <div data-infinite-marquee>
    //     <img src="logo-1.svg" alt="Acme"> <img src="logo-2.svg" alt="Globex"> …
    //   </div>
    // Optional: data-speed (px per second, default 60), data-direction="right",
    // data-gap (space between items, default 2.5em), data-fade="false" (no faded
    // edges), data-pause-on-hover="false" (keep moving on hover).
    var DEFAULTS = { speed: 60, gap: '2.5em' };

    var css =
      '.imq{overflow:hidden;display:flex}' +
      '.imq-fade{-webkit-mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent);mask-image:linear-gradient(90deg,transparent,#000 10%,#000 90%,transparent)}' +
      '.imq-track{display:flex;flex:none;width:max-content;will-change:transform}' +
      '.imq-group{display:flex;flex:none;align-items:center;gap:var(--imq-gap);padding-right:var(--imq-gap)}' +
      '.imq-group>*{flex:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function setup(el) {
      var d = el.dataset;
      el.classList.add('imq');
      if (d.fade !== 'false') el.classList.add('imq-fade');
      el.style.setProperty('--imq-gap', d.gap || DEFAULTS.gap);
      var track = document.createElement('div');
      track.className = 'imq-track';
      var group = document.createElement('div');
      group.className = 'imq-group';
      while (el.firstChild) group.appendChild(el.firstChild);
      track.appendChild(group);
      el.appendChild(track);
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;

      var anim = null, groupW = 0, visible = false, hovered = false;
      var dir = d.direction === 'right' ? 1 : -1;

      function layout() {
        var w = group.offsetWidth;
        if (!w || w === groupW) return;
        groupW = w;
        // Enough copies to fill the row twice over, so the wrap is never seen.
        track.querySelectorAll('.imq-clone').forEach(function (c) { c.remove(); });
        var copies = Math.max(1, Math.ceil(el.clientWidth / w)) + 1;
        for (var i = 0; i < copies; i++) {
          var c = group.cloneNode(true);
          c.classList.add('imq-clone');
          c.setAttribute('aria-hidden', 'true');
          c.querySelectorAll('a,button,input,select,textarea,[tabindex]').forEach(function (f) { f.setAttribute('tabindex', '-1'); });
          track.appendChild(c);
        }
        if (anim) anim.revert();
        anim = anime.animate(track, {
          x: dir < 0 ? [0, -w] : [-w, 0],
          duration: (w / (+d.speed || DEFAULTS.speed)) * 1000,
          ease: 'linear',
          loop: true,
          autoplay: visible
        });
      }
      layout();
      if (window.ResizeObserver) new ResizeObserver(layout).observe(el);
      // Fonts and images can change the width after load.
      window.addEventListener('load', layout);

      if (d.pauseOnHover !== 'false') {
        // Ease the speed down and back up rather than stopping dead.
        el.addEventListener('pointerenter', function () { hovered = true; if (anim) anime.animate(anim, { speed: 0.001, duration: 500, ease: 'out(2)' }); });
        el.addEventListener('pointerleave', function () { hovered = false; if (anim) anime.animate(anim, { speed: 1, duration: 500, ease: 'in(2)' }); });
      }
      new IntersectionObserver(function (entries) {
        visible = entries[0].isIntersecting;
        if (!anim) return;
        if (visible) anim.play(); else anim.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-infinite-marquee]').forEach(function (el) {
        if (el._imq) return;
        el._imq = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-infinite-marquee>
  <img src="acme.svg" alt="Acme">
  <img src="globex.svg" alt="Globex">
  <img src="initech.svg" alt="Initech">
</div>