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.
Setup — External Scripts
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script><script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>Code
<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>.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;
}
}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
| Name | Type | Default | Description |
|---|---|---|---|
| [data-logo-wall-cycle-init] | attribute | — | The 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] | attribute | — | The element that wraps all logo items. The script queries this for items and tracks their visibility. |
| [data-logo-wall-item] | attribute | — | Each 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] | attribute | — | Optional 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] | attribute | — | The 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.