Marquee Infinito con CSS: Scorrimento Loghi e Testi Senza JS
Perché il Marquee Infinito CSS nel 2026
Se stai costruendo un sito vetrina, una landing page o la home di un prodotto SaaS, quasi certamente hai visto quella sezione con i loghi dei clienti che scorrono in loop. Prima si usava JavaScript, poi librerie come Swiper o Slick Slider, oggi — nel 2026 — puoi ottenere lo stesso effetto con sole 10 righe di CSS. Nessuna dipendenza, nessun bundle size aggiuntivo, zero problemi di hydration nei framework moderni.
L’idea di base è semplice: si usa @keyframes per traslare un contenitore di elementi da destra a sinistra in modo continuo, e si duplica il set di elementi per creare l’illusione del loop infinito. Il risultato è percettivamente identico all’approccio JavaScript, ma con prestazioni migliori perché il browser gestisce l’animazione sul compositor thread, senza toccare il main thread. Per chi lavora su siti vetrina — come discusso nella guida sulle landing page per Micro SaaS — questo tipo di componente è quasi sempre richiesto dai clienti e vale la pena padroneggiarlo una volta sola. In questo articolo vedremo la tecnica base, le varianti avanzate (pause on hover, direzione inversa, velocità variabile) e come evitare i difetti visivi più comuni.
La Struttura HTML Corretta per il Marquee CSS
Prima del CSS, serve una struttura HTML pensata. Il pattern più robusto prevede un wrapper esterno con overflow: hidden e un track interno che contiene gli elementi duplicati. Ecco lo scheletro:
<!-- Wrapper con overflow hidden -->
<div class="marquee">
<!-- Track: contiene 2 set identici per il loop seamless -->
<div class="marquee__track">
<!-- Set originale -->
<div class="marquee__item">Logo A</div>
<div class="marquee__item">Logo B</div>
<div class="marquee__item">Logo C</div>
<div class="marquee__item">Logo D</div>
<div class="marquee__item">Logo E</div>
<!-- Set duplicato: aria-hidden per accessibilità -->
<div class="marquee__item" aria-hidden="true">Logo A</div>
<div class="marquee__item" aria-hidden="true">Logo B</div>
<div class="marquee__item" aria-hidden="true">Logo C</div>
<div class="marquee__item" aria-hidden="true">Logo D</div>
<div class="marquee__item" aria-hidden="true">Logo E</div>
</div>
</div>Il punto chiave è aria-hidden="true" sul secondo set: i lettori screen reader vedranno ogni logo una sola volta, non duplicato. La classe marquee__track deve essere abbastanza larga da contenere entrambi i set — vedremo come gestirlo con width: max-content.
Il CSS Base: Animazione Loop Infinito
Ecco il CSS minimo funzionante. La tecnica usa translate X del 50% negativo del track — che corrisponde esattamente alla larghezza del primo set — creando un loop perfettamente seamless:
/* ========================================
MARQUEE INFINITO - CSS Base 2026
======================================== */
.marquee {
overflow: hidden;
/* Fade ai bordi con mask gradient */
-webkit-mask-image: linear-gradient(
to right,
transparent 0%,
black 10%,
black 90%,
transparent 100%
);
mask-image: linear-gradient(
to right,
transparent 0%,
black 10%,
black 90%,
transparent 100%
);
}
.marquee__track {
display: flex;
width: max-content; /* si espande quanto serve */
gap: 3rem;
animation: marquee-scroll 30s linear infinite;
will-change: transform; /* hint al browser per il compositor */
}
.marquee__item {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
padding: 1rem 2rem;
}
/* Keyframe: trasla di metà del track (= un set completo) */
@keyframes marquee-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
/* Accessibilità: rispetta prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
.marquee__track {
animation-duration: 120s;
}
}Il trucco del -50% funziona perché il track contiene esattamente due set identici: traslandolo del 50%, il secondo set occupa esattamente la posizione dove stava il primo. L’animazione poi riparte da zero e il ciclo è perfetto. Il mask gradient ai bordi è una finitura professionale che ammorbidisce gli estremi e dà l’impressione di un nastro continuo.
🔧 Pro tip: usa
will-change: transformcon parsimonia — crea un nuovo layer compositing. Se hai molti marquee sulla pagina, applicalo solo a quello in viewport usando una classe aggiunta viaIntersectionObserver.
Pause on Hover e Controllo Velocità con CSS Variables
Una delle richieste più comuni è la pausa al passaggio del mouse. Con CSS variables si ottiene in modo elegante senza toccare la struttura HTML:
/* ========================================
MARQUEE con CSS Variables e Pause Hover
======================================== */
:root {
--marquee-duration: 30s;
--marquee-gap: 3rem;
--marquee-direction: normal; /* oppure: reverse */
}
.marquee {
overflow: hidden;
-webkit-mask-image: linear-gradient(
to right, transparent 0%, black 8%, black 92%, transparent 100%
);
mask-image: linear-gradient(
to right, transparent 0%, black 8%, black 92%, transparent 100%
);
}
.marquee__track {
display: flex;
width: max-content;
gap: var(--marquee-gap);
animation: marquee-scroll var(--marquee-duration) linear infinite;
animation-direction: var(--marquee-direction);
}
/* Pausa al hover: animation-play-state */
.marquee:hover .marquee__track {
animation-play-state: paused;
}
/* Variante veloce (per testi più brevi) */
.marquee--fast {
--marquee-duration: 15s;
}
/* Variante inversa (scorrimento da sx a dx) */
.marquee--reverse {
--marquee-direction: reverse;
}
/* Marquee più lento per loghi grandi */
.marquee--slow {
--marquee-duration: 50s;
}
@keyframes marquee-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}Usare CSS custom properties come --marquee-duration e --marquee-direction rende il componente estremamente configurabile: basta aggiungere una classe modificatrice o sovrascrivere la variabile inline su ogni istanza. È lo stesso pattern descritto nella guida su variabili CSS per temi adattivi — un approccio che si scala bene in qualsiasi design system.
Marquee Verticale e Multi-Riga
Lo stesso principio funziona in verticale — utile per ticker di notizie o feed laterali — e in configurazioni multi-riga dove due righe scorrono in direzioni opposte per un effetto più dinamico:
/* ========================================
MARQUEE VERTICALE
======================================== */
.marquee--vertical {
overflow: hidden;
height: 400px; /* altezza fissa del contenitore */
-webkit-mask-image: linear-gradient(
to bottom, transparent 0%, black 10%, black 90%, transparent 100%
);
mask-image: linear-gradient(
to bottom, transparent 0%, black 10%, black 90%, transparent 100%
);
}
.marquee--vertical .marquee__track {
display: flex;
flex-direction: column; /* elementi in colonna */
width: auto;
height: max-content; /* altezza auto come width prima */
gap: 1.5rem;
animation: marquee-vertical 20s linear infinite;
}
@keyframes marquee-vertical {
from { transform: translateY(0); }
to { transform: translateY(-50%); }
}
/* ========================================
DOPPIA RIGA DIREZIONI OPPOSTE
======================================== */
.marquee-stack {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.marquee-stack .marquee:nth-child(odd) .marquee__track {
animation-direction: normal;
}
.marquee-stack .marquee:nth-child(even) .marquee__track {
animation-direction: reverse;
}
/* Velocità leggermente diverse per effetto organico */
.marquee-stack .marquee:nth-child(1) .marquee__track { animation-duration: 25s; }
.marquee-stack .marquee:nth-child(2) .marquee__track { animation-duration: 35s; }
.marquee-stack .marquee:nth-child(3) .marquee__track { animation-duration: 20s; }L’effetto doppia riga con direzioni opposte è molto usato nelle sezioni “trusted by” di prodotti moderni. Piccola variazione: impostare velocità leggermente diverse tra le righe (25s vs 35s) crea un ritmo naturale che sembra meno meccanico. Per approfondire le tecniche di animazione scroll-based, leggi anche la guida sulle animazioni legate allo scroll con view-timeline.
Ottimizzare le Performance: Evitare i Difetti Comuni
Ci sono alcuni errori che degradano le performance o causano glitch visivi. Ecco i più frequenti e come evitarli:
- Non usare
marginper il gap — usagapsu flexbox. I margini cambiano la larghezza effettiva del contenitore e possono causare salti al loop. - Non animare
leftomargin-left— usa sempretransform: translateX(). Le proprietà geometriche forzano layout e paint, transform opera solo sul compositor. - Non mettere
will-changesu ogni elemento — solo sul track, non sui singoli item. - Immagini SVG senza larghezza esplicita — i loghi SVG devono avere
widtheheightfissi o il browser calcola male la larghezza del track. - Gap non coerente tra set originale e duplicato — se usi padding invece di gap, assicurati che l’ultimo item del primo set e il primo del secondo abbiano la stessa spaziatura degli altri.
Un’altra ottimizzazione importante è stoppare l’animazione fuori dal viewport. Anche se i browser moderni ottimizzano le animazioni non visibili, è buona pratica usare IntersectionObserver per aggiungere/rimuovere una classe che imposta animation-play-state: paused. Questo pattern è simile a quello usato per il lazy load con Intersection Observer — stesso observer, logica analoga.
Implementazione Completa: Sezione Loghi Clienti
Ecco un esempio completo e production-ready di una sezione “trusted by” con loghi SVG, stile scuro, fade ai bordi e pausa hover. Questo è il codice che puoi copiare direttamente in un progetto:
<!-- HTML -->
<section class="clients-section">
<p class="clients-section__label">Usato da oltre 200 aziende</p>
<div class="marquee">
<div class="marquee__track">
<!-- Set 1 -->
<img class="marquee__logo" src="/loghi/acme.svg" alt="Acme Corp" width="120" height="40">
<img class="marquee__logo" src="/loghi/globex.svg" alt="Globex" width="120" height="40">
<img class="marquee__logo" src="/loghi/initech.svg" alt="Initech" width="120" height="40">
<img class="marquee__logo" src="/loghi/umbrella.svg" alt="Umbrella" width="120" height="40">
<img class="marquee__logo" src="/loghi/hooli.svg" alt="Hooli" width="120" height="40">
<img class="marquee__logo" src="/loghi/vehement.svg" alt="Vehement" width="120" height="40">
<!-- Set 2 - duplicato, aria-hidden -->
<img class="marquee__logo" src="/loghi/acme.svg" alt="" width="120" height="40" aria-hidden="true">
<img class="marquee__logo" src="/loghi/globex.svg" alt="" width="120" height="40" aria-hidden="true">
<img class="marquee__logo" src="/loghi/initech.svg" alt="" width="120" height="40" aria-hidden="true">
<img class="marquee__logo" src="/loghi/umbrella.svg" alt="" width="120" height="40" aria-hidden="true">
<img class="marquee__logo" src="/loghi/hooli.svg" alt="" width="120" height="40" aria-hidden="true">
<img class="marquee__logo" src="/loghi/vehement.svg" alt="" width="120" height="40" aria-hidden="true">
</div>
</div>
</section>
<!-- CSS -->
<style>
.clients-section {
padding: 4rem 0;
background: #0f172a;
}
.clients-section__label {
text-align: center;
font-size: 0.875rem;
color: #64748b;
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 2rem;
}
.marquee {
overflow: hidden;
-webkit-mask-image: linear-gradient(to right,transparent 0%,#000 8%,#000 92%,transparent 100%);
mask-image: linear-gradient(to right,transparent 0%,#000 8%,#000 92%,transparent 100%);
}
.marquee__track {
display: flex;
width: max-content;
gap: 5rem;
align-items: center;
animation: marquee-scroll 35s linear infinite;
}
.marquee:hover .marquee__track {
animation-play-state: paused;
}
.marquee__logo {
opacity: 0.5;
filter: grayscale(1);
transition: opacity 0.3s, filter 0.3s;
}
.marquee__logo:hover {
opacity: 1;
filter: grayscale(0);
}
@keyframes marquee-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
.marquee__track { animation-duration: 120s; }
}
</style>Nota il dettaglio di opacity: 0.5; filter: grayscale(1) sui loghi a riposo e il ripristino all’hover: è un pattern comune che riduce la competizione visiva e valorizza i loghi quando l’utente ci interagisce. Il secondo set usa alt="" invece di aria-hidden direttamente sull’img — entrambi funzionano, ma alt="" è più compatibile con vecchi screen reader. Se vuoi approfondire le best practice sull’accessibilità nei componenti interattivi, vedi la guida su aria-label e accessibilità frontend.
💡 Tip performance: se i tuoi loghi sono SVG inline (non
img), avvolgi ogni logo in undivcon larghezza fissa. Gli SVG inline senza viewBox dichiarata possono avere dimensioni imprevedibili e rompere il calcolo del 50%.
Usarlo in React, Vue e Astro
In un framework moderno conviene astrarre il marquee in un componente riutilizzabile. Ecco l’implementazione React — compatibile con Next.js (App Router incluso) senza nessun “use client” se si lascia il CSS in un modulo separato:
// components/Marquee.jsx
// Componente CSS-only, zero dipendenze JS per l'animazione
export function Marquee({
items,
speed = 30, // secondi per ciclo completo
reverse = false,
pauseOnHover = true,
gap = "3rem",
className = "",
}) {
const trackStyle = {
"--marquee-duration": `${speed}s`,
"--marquee-gap": gap,
"--marquee-direction": reverse ? "reverse" : "normal",
};
return (
<div
className={`marquee ${className}`}
style={{ "--pause-on-hover": pauseOnHover ? "paused" : "running" }}
>
<div className="marquee__track" style={trackStyle}>
{/* Set originale */}
{items.map((item, i) => (
<div key={i} className="marquee__item">{item}</div>
))}
{/* Set duplicato per loop seamless */}
{items.map((item, i) => (
<div key={`dup-${i}`} className="marquee__item" aria-hidden="true">
{item}
</div>
))}
</div>
</div>
);
}
// CSS (marquee.module.css oppure in globals.css)
/*
.marquee {
overflow: hidden;
mask-image: linear-gradient(to right, transparent, black 10%, black 90%, transparent);
}
.marquee__track {
display: flex;
width: max-content;
gap: var(--marquee-gap, 3rem);
animation: marquee-scroll var(--marquee-duration, 30s) linear infinite;
animation-direction: var(--marquee-direction, normal);
}
.marquee:hover .marquee__track {
animation-play-state: var(--pause-on-hover, running);
}
@keyframes marquee-scroll {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
*/
// Uso:
// <Marquee items={logos} speed={40} reverse pauseOnHover />Il componente accetta le props come semplice configurazione e le trasforma in CSS variables inline. Tutto il movimento vive nel CSS: questo significa che il componente è compatibile con i Server Components di React 19 — non serve il marcatore "use client" perché non c’è JavaScript che gira nel browser per l’animazione. Per approfondire i pattern moderni di React, vedi la guida su React 19 e Server Components.
🎯 Strategia: per siti vetrina con molti loghi, carica le immagini con
loading="lazy"solo sul secondo set duplicato. Il set originale deve essere visibile immediatamente, il duplicato può caricarsi un attimo dopo senza impatto visibile.
FAQ e Domande Frequenti
Perché il marquee si blocca per un frame alla fine del loop?
Il problema classico del “jump” è quasi sempre causato da un calcolo errato del 50%. Accade quando gli elementi hanno margin invece di gap, o quando le immagini SVG non hanno dimensioni dichiarate esplicitamente. Verifica che il track contenga esattamente due set di elementi identici (stesso numero, stesse dimensioni, stesso gap) e usa gap su flexbox invece di margin. Se il problema persiste, apri DevTools e controlla la larghezza computata del track: deve essere pari al doppio della larghezza del singolo set.
Quanti elementi servono prima di dover duplicare più di due volte?
Con due set funziona finché il primo set è più largo del viewport. Se hai solo 3-4 loghi piccoli, la larghezza totale del set potrebbe essere inferiore alla larghezza dello schermo e vedrai un gap prima del loop. La soluzione è duplicare il set più volte (3 o 4 copie) oppure aumentare il gap o la dimensione degli item. Una regola pratica: il singolo set deve essere almeno 1.2× la larghezza del viewport più grande che devi supportare.
Come rendere la velocità proporzionale al numero di elementi?
La velocità percepita dipende sia dalla durata dell’animazione sia dalla larghezza del track. Per mantenere una velocità in pixel/secondo costante indipendentemente dal numero di elementi, devi calcolare la durata dinamicamente: durata = larghezza_set / velocità_px_per_s. Questo richiede un minimo di JavaScript per leggere la larghezza computata del set. In alternativa, una regola empirica è impostare circa 5-6 secondi per ogni elemento nel set — con 6 loghi, 30-36 secondi sono una velocità gradevole.
Il marquee CSS funziona su tutti i browser?
Sì, il pattern base (flexbox + transform + @keyframes + animation) ha supporto universale da anni. L’unica proprietà leggermente più recente è mask-image per il fade ai bordi, che richiede il prefisso -webkit-mask-image per Safari — incluso nell’esempio. width: max-content è supportato da tutti i browser moderni. Se devi supportare IE11 (caso sempre più raro nel 2026), serve un approccio diverso basato su JavaScript, ma per qualsiasi target moderno il CSS è sufficiente.
Conclusione
Il marquee infinito in puro CSS è uno di quei componenti che sembra complesso ma si riduce a un pattern di 20-30 righe una volta capito il meccanismo del 50%. La chiave è la struttura HTML con duplicazione degli elementi, l’uso di gap su flexbox e il keyframe che trasla esattamente metà del track. Le varianti — pausa hover, direzione inversa, doppia riga, verticale — si costruiscono tutte sopra lo stesso nucleo cambiando poche variabili CSS.
Per i siti vetrina, dove questo componente è quasi sempre richiesto, avere una versione CSS-only pronta significa zero dipendenze aggiuntive, bundle size invariato e animazioni fluide su qualsiasi dispositivo. Se stai scalando come freelance e vuoi offrire componenti di qualità ai tuoi clienti, considera di costruire una libreria personale di questi snippet — sarà un vantaggio competitivo concreto nella gestione dei tuoi progetti.
Suggerimenti e Risorse
🔧 Strumento: usa il DevTools Performance panel per verificare che l’animazione giri sul compositor thread (cerchi un layer separato nel Layers panel). Se vedi “Paint” o “Layout” nei record, stai animando la proprietà sbagliata.
💡 Approfondimento CSS: per costruire componenti CSS riutilizzabili e mantenibili, leggi la guida alle variabili CSS per temi adattivi — lo stesso approccio si applica a qualsiasi componente animato.
🎯 Prossimo step: se vuoi aggiungere animazioni più elaborate alle tue landing page, esplora le animazioni CSS moderne per effetti che valorizzano l’UI senza JavaScript aggiuntivo.

