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
- Paste the snippet just before
</head>. - Add
data-infinite-marqueeto a container holding the items: logos, words, cards.
Options
Add any of these attributes to the same element.
data-speed | Pixels per second. Default 60. |
data-direction="right" | Move to the right instead of the left. |
data-gap | Space 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.
<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><div data-infinite-marquee>
<img src="acme.svg" alt="Acme">
<img src="globex.svg" alt="Globex">
<img src="initech.svg" alt="Initech">
</div>