Logo Wall Cycle

A logo grid that automatically cycles through a larger set of logos by swapping them in and out one at a time with a smooth slide-and-fade animation. Configurable shuffle order, responsive visible count via CSS, and ScrollTrigger-aware pause/resume.

gsapscrolltriggerlogogridanimationcyclingcms

Setup — External Scripts

CDN — GSAP (add before </body>)
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
CDN — ScrollTrigger (add after GSAP)
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>

Code

HTML
html
<div data-logo-wall-shuffle="false" data-logo-wall-cycle-init="" class="logo-wall">
  <div class="logo-wall__collection">
    <div data-logo-wall-list="" class="logo-wall__list">
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370ea9d37fbceb3be49cb_logo-webflow.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370ea48d4fb0c708dd1dc_logo-microsoft.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370ea9ba384ff47fa5d51_logo-asana.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370eaec918fbd4a0acc12_logo-snapchat.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370ea155a551c08692a03_logo-google.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370eafdf2b295d65f9450_logo-bluesky.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370ea68a433ee5808ed90_logo-codepen.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370ea2ebc0415055d04f3_logo-linkedin.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370ea7699561e6f9f008f_logo-android.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370ea753f2afe2f6b036f_logo-apple.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370eabec1e0c00348b5ed_logo-twitter.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370ea0e0e1dc81a9b5799_logo-osmo.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370ea36c91584afe43e2d_logo-medium.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370ea87b05cdce0387084_logo-eventbrite.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370eaf4465d763c2f9b2a_logo-behance.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
      <div data-logo-wall-item="" class="logo-wall__item">
        <div data-logo-wall-target-parent="" class="logo-wall__logo">
          <div class="logo-wall__logo-before"></div>
          <div data-logo-wall-target="" class="logo-wall__logo-target">
            <img src="https://cdn.prod.website-files.com/68836e3f51ac98fec14ceed2/688370eaec1d445957d7e3a1_logo-chatgpt.svg" loading="lazy" width="100" alt="" class="logo-wall__logo-img">
          </div>
        </div>
      </div>
    </div>
  </div>
</div>
CSS
css
.logo-wall {
  display: flex;
  justify-content: center;
  width: 100%;
}

.logo-wall__collection {
  width: 100%;
}

.logo-wall__list {
  display: flex;
  flex-flow: wrap;
}

.logo-wall__item {
  width: 25%;
  position: relative;
}

[data-logo-wall-list] [data-logo-wall-item]:nth-child(n+9) {
  display: none;
}

.logo-wall__logo {
  display: flex;
  justify-content: center;
  align-items: center;
  position: relative;
}

.logo-wall__logo-before {
  padding-top: 66.66%;
}

.logo-wall__logo-target {
  justify-content: center;
  align-items: center;
  width: 66.66%;
  height: 40%;
  display: flex;
  position: absolute;
}

.logo-wall__logo-img {
  width: 100%;
  height: 100%;
  max-height: 100%;
}

@media screen and (max-width: 991px) {
  .logo-wall__item {
    width: 33.333%;
  }

  [data-logo-wall-list] [data-logo-wall-item]:nth-child(n+7) {
    display: none;
  }
}
JavaScript
javascript
function initLogoWallCycle() {
  const loopDelay = 1.5;   // Loop Duration
  const duration  = 0.9;   // Animation Duration

  document.querySelectorAll('[data-logo-wall-cycle-init]').forEach(root => {
    const list   = root.querySelector('[data-logo-wall-list]');
    const items  = Array.from(list.querySelectorAll('[data-logo-wall-item]'));

    const shuffleFront = root.getAttribute('data-logo-wall-shuffle') !== 'false';
    const originalTargets = items
      .map(item => item.querySelector('[data-logo-wall-target]'))
      .filter(Boolean);

    let visibleItems   = [];
    let visibleCount   = 0;
    let pool           = [];
    let pattern        = [];
    let patternIndex   = 0;
    let tl;

    function isVisible(el) {
      return window.getComputedStyle(el).display !== 'none';
    }

    function shuffleArray(arr) {
      const a = arr.slice();
      for (let i = a.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [a[i], a[j]] = [a[j], a[i]];
      }
      return a;
    }

    function setup() {
      if (tl) {
        tl.kill();
      }
      visibleItems = items.filter(isVisible);
      visibleCount = visibleItems.length;

      pattern = shuffleArray(
        Array.from({ length: visibleCount }, (_, i) => i)
      );
      patternIndex = 0;

      // remove all injected targets
      items.forEach(item => {
        item.querySelectorAll('[data-logo-wall-target]').forEach(old => old.remove());
      });

      pool = originalTargets.map(n => n.cloneNode(true));

      let front, rest;
      if (shuffleFront) {
        const shuffledAll = shuffleArray(pool);
        front = shuffledAll.slice(0, visibleCount);
        rest  = shuffleArray(shuffledAll.slice(visibleCount));
      } else {
        front = pool.slice(0, visibleCount);
        rest  = shuffleArray(pool.slice(visibleCount));
      }
      pool = front.concat(rest);

      for (let i = 0; i < visibleCount; i++) {
        const parent =
          visibleItems[i].querySelector('[data-logo-wall-target-parent]') ||
          visibleItems[i];
        parent.appendChild(pool.shift());
      }

      tl = gsap.timeline({ repeat: -1, repeatDelay: loopDelay });
      tl.call(swapNext);
      tl.play();
    }

    function swapNext() {
      const nowCount = items.filter(isVisible).length;
      if (nowCount !== visibleCount) {
        setup();
        return;
      }
      if (!pool.length) return;

      const idx = pattern[patternIndex % visibleCount];
      patternIndex++;

      const container = visibleItems[idx];
      const parent =
        container.querySelector('[data-logo-wall-target-parent]') ||
        container.querySelector('*:has(> [data-logo-wall-target])') ||
        container;
      const existing = parent.querySelectorAll('[data-logo-wall-target]');
      if (existing.length > 1) return;

      const current  = parent.querySelector('[data-logo-wall-target]');
      const incoming = pool.shift();

      gsap.set(incoming, { yPercent: 50, autoAlpha: 0 });
      parent.appendChild(incoming);

      if (current) {
        gsap.to(current, {
          yPercent: -50,
          autoAlpha: 0,
          duration,
          ease: "expo.inOut",
          onComplete: () => {
            current.remove();
            pool.push(current);
          }
        });
      }

      gsap.to(incoming, {
        yPercent: 0,
        autoAlpha: 1,
        duration,
        delay: 0.1,
        ease: "expo.inOut"
      });
    }

    setup();

    ScrollTrigger.create({
      trigger: root,
      start: 'top bottom',
      end: 'bottom top',
      onEnter:     () => tl.play(),
      onLeave:     () => tl.pause(),
      onEnterBack: () => tl.play(),
      onLeaveBack: () => tl.pause()
    });

    document.addEventListener('visibilitychange', () =>
      document.hidden ? tl.pause() : tl.play()
    );
  });
}

// Initialize Logo Wall Cycle
document.addEventListener('DOMContentLoaded', () => {
  initLogoWallCycle();
});

Attributes

NameTypeDefaultDescription
[data-logo-wall-cycle-init]attributeThe outermost wrapper element. Add this to initialize a single logo wall animation instance.
[data-logo-wall-shuffle]"true" | "false""true"When set to "true", the initially visible logos are also shuffled. Set to "false" to preserve the original order for the first visible set.
[data-logo-wall-list]attributeThe element that wraps all logo items. The script queries this for items and tracks their visibility.
[data-logo-wall-item]attributeEach individual logo entry. Acts as a placeholder slot for the rotating logos. The script determines visibility via CSS display: none.
[data-logo-wall-target-parent]attributeOptional wrapper inside each item that controls placement. Incoming and outgoing logos are appended to this element. If omitted, logos are appended directly to [data-logo-wall-item].
[data-logo-wall-target]attributeThe element that gets animated in and out during each swap. This is cloned from the original list and cycled through the pool.

Notes

  • Requires GSAP and ScrollTrigger loaded via CDN before the script runs.
  • The animation automatically pauses when the component scrolls out of the viewport and resumes when it returns — no wasted animation when off-screen.
  • The script detects responsive breakpoint changes: if the visible item count changes (e.g. on resize), it calls setup() again automatically.
  • Multiple [data-logo-wall-cycle-init] instances on the same page are fully supported — each gets its own independent timeline and pool.
  • When document.hidden is true (tab switch, minimize), the timeline pauses automatically.

Guide

Container

Add the [data-logo-wall-cycle-init] attribute to the outermost wrapper. This initializes a single logo wall animation instance. You can configure shuffling behavior with the optional [data-logo-wall-shuffle] attribute. When enabled, the first visible logos are shuffled too.

List

Apply [data-logo-wall-list] to the element that wraps all logo items.

Item

Each logo entry requires [data-logo-wall-item]. This acts as a placeholder for rotating logos. The script uses CSS display: none to decide which logos are excluded from the visible set.

/* Desktop: Show only first 8 */
[data-logo-wall-list] > [data-logo-wall-item]:nth-child(n+9) {
  display: none;
}

/* Tablet/mobile: Show only first 6 */
@media (max-width: 768px) {
  [data-logo-wall-list] > [data-logo-wall-item]:nth-child(n+7) {
    display: none;
  }
}

Target & Target Parent

Inside each item, add a [data-logo-wall-target] element. This is the element that gets animated in/out during swaps. You can wrap it in a parent marked with [data-logo-wall-target-parent] for more placement control. When this attribute is not added, the next logo will be placed in the [data-logo-wall-item] element.

Customizing the Animation

This example uses a slide-and-fade animation to cycle logos in and out. You can easily replace it with any animation style that better suits your project.