Get Your Audit

EN

RU

Soon

Scroller

Infinite running rows, in pure JavaScript

Marquee

Vanilla JS

Responsive

A tiny, dependency-free library that turns any row of elements into a smooth, seamless infinite marquee . It clones your content automatically, runs on the Web Animations API, and respects prefers-reduced-motion . It works with any markup — cards, logos, text, images. The demos below use plain empty tiles on purpose, so you can see the motion itself.

Live demos

Every example below is the same script with a different config. Hover the interactive ones.

Horizontal · Left default, with edge fade direction: 'left'
1
2
3
4
5
6
7
8
Horizontal · Right reversed travel direction: 'right'
1
2
3
4
5
6
7
8
Pause on hover hover to freeze instantly pauseOnHover: true
1
2
3
4
5
6
7
8
Slow on hover eases down to ¼ speed slowOnHover: true
1
2
3
4
5
6
7
8
Edge fade default mask removeMask: false
1
2
3
4
5
6
7
8
No mask hard edges removeMask: true
1
2
3
4
5
6
7
8
Vertical · Up direction: 'up'
1
2
3
4
5
6
Vertical · Down direction: 'down'
1
2
3
4
5
6

1 · HTML structure

The script needs this exact nesting: an outer container and a .scroller-wrapper holding your items.

HTML
<div class="my-scroller">
  <div class="scroller-wrapper">
    <!-- Your items: cards, logos, text -->
    <div class="item">1</div>
    <div class="item">2</div>
    <div class="item">3</div>
  </div>
</div>

2 · CSS (required)

Add these styles once. They set the layout, the edge-fade mask, and the orientation switches the script toggles via data-attributes.

CSS
/*! Scroller by leggo.dev v2.2 */
.scroller-wrapper { display: flex; }

[data-animated="true"] { overflow: hidden; }

[data-animated="true"][data-orientation="horizontal"]:not([data-mask-disabled="true"]) {
  -webkit-mask: linear-gradient(90deg, transparent, white 20%, white 80%, transparent);
          mask: linear-gradient(90deg, transparent, white 20%, white 80%, transparent);
}
[data-animated="true"][data-orientation="horizontal"] .scroller-wrapper {
  width: max-content;
  flex-wrap: nowrap;
}
[data-animated="true"][data-orientation="vertical"]:not([data-mask-disabled="true"]) {
  -webkit-mask: linear-gradient(0deg, transparent, white 20%, white 80%, transparent);
          mask: linear-gradient(0deg, transparent, white 20%, white 80%, transparent);
}
[data-animated="true"][data-orientation="vertical"] .scroller-wrapper {
  height: max-content;
  flex-direction: column;
  flex-wrap: nowrap;
}

3 · Settings

Every option is optional — pass only what you need. breakpoints overrides any option per screen width.

Option Type Default Description
direction String 'left' Travel direction: 'left' , 'right' , 'up' , 'down' .
pixelsPerSecond Number 50 Speed of movement — higher is faster.
gap Number 2 Space between items, in rem .
duplicates Number 1 How many times to clone the set (minimum 1) for a seamless loop.
pauseOnHover Boolean false Stop the animation while the cursor is over it.
slowOnHover Boolean false Slow down on hover instead of stopping.
slowdownFactor Number 3 How many times to slow down (when slowOnHover: true ).
removeMask Boolean false Remove the fade (mask) on the edges for hard cut-offs.
breakpoints Object {} Per-screen-width overrides for any of the options above.

4 · Initialization

Call initMyScroller() with a selector and your config. breakpoints let you change speed, gap or duplicates as the viewport grows.

JavaScript
initMyScroller('.my-scroller', {
  direction: 'left',
  pixelsPerSecond: 60,
  gap: 2,
  pauseOnHover: true,
  breakpoints: {
    768:  { pixelsPerSecond: 100, gap: 3 }, // tablets and up
    1024: { duplicates: 2 }                 // desktops
  }
});

5 · The library (v2.7)

Drop this in once, before your initMyScroller call (or in your global JS). Stability-tested; it re-measures on resize and bails on reduced-motion.

Good to know:

  • It clones your items automatically — write each item only once.
  • It waits for any inside to load before measuring, so the loop never jumps.
  • Users with “reduce motion” on simply see a static, scroll-free row.
JavaScript
/* Scroller by leggo.dev v2.7 */
function initMyScroller(s,t={}){function n(e){if(!e.fullConfig)return;const r=e=>{const r={...e},n=window.innerWidth,o=e.breakpoints||{};return Object.keys(o).sort(((e,t)=>e-t)).forEach((e=>{n>=e&&Object.assign(r,o[e])})),r},o=r(e.fullConfig);if(e.scrollerAnimation&&e.currentAppliedConfig===JSON.stringify(o))return;e.currentAppliedConfig=JSON.stringify(o),e.scrollerAnimation?.cancel(),e.eventController?.abort(),e.eventController=new AbortController;const{signal:i}=e.eventController,l=e.querySelector(".scroller-wrapper");if(!l)return;e.originalWrapperHTML?l.innerHTML=e.originalWrapperHTML:e.originalWrapperHTML=l.innerHTML;const a=o.direction||"left",c=o.gap||2,d=o.duplicates||1,p=o.pixelsPerSecond||50,f="up"===a||"down"===a?"vertical":"horizontal";e.setAttribute("data-animated","true"),e.setAttribute("data-orientation",f),o.removeMask&&e.setAttribute("data-mask-disabled","true"),l.style.display="flex","vertical"===f?(l.style.flexDirection="column",l.style.rowGap=c+"rem",l.style.columnGap="0"):(l.style.flexDirection="row",l.style.columnGap=c+"rem",l.style.rowGap="0");const u=l.querySelectorAll("img"),m=Array.from(u).map((e=>e.complete?Promise.resolve():new Promise((t=>{e.onload=t,e.onerror=t}))));Promise.all(m).then((()=>{requestAnimationFrame((()=>{const r=parseFloat(getComputedStyle(document.documentElement).fontSize)*c,n="vertical"===f?l.scrollHeight:l.scrollWidth;if(n{const t=e.cloneNode(!0);t.setAttribute("aria-hidden","true"),l.appendChild(t)}))}const u=n+r,m=(u/p)*1000,w="vertical"===f?"translateY":"translateX",v=l.animate([{transform:`${w}(0)`},{transform:`${w}(-${u}px)`}],{duration:m,iterations:1/0,easing:"linear",direction:"right"===a||"down"===a?"reverse":"normal"});e.scrollerAnimation=v,o.pauseOnHover?(e.addEventListener("mouseenter",(()=>v.pause()),{signal:i}),e.addEventListener("mouseleave",(()=>v.play()),{signal:i})):o.slowOnHover&&(e.addEventListener("mouseenter",(()=>v.playbackRate=1/(o.slowdownFactor||3)),{signal:i}),e.addEventListener("mouseleave",(()=>v.playbackRate=1),{signal:i}))}))}))}const o=document.querySelectorAll(s);if(!window.matchMedia("(prefers-reduced-motion: reduce)").matches){let i=window.innerWidth;o.forEach((e=>{e.fullConfig=t,n(e),window.addEventListener("resize",(()=>{window.innerWidth!==i&&(i=window.innerWidth,n(e))}))}))}}