Nessun risultato. Prova con un altro termine.
Guide
Notizie
Software
Tutorial

Controllare CSS Scroll Snap e Scroll-Driven Animation con JavaScript

CSS guida lo scroll, JavaScript osserva, coordina e personalizza dove necessario all'interno di un'applicazione web
CSS guida lo scroll, JavaScript osserva, coordina e personalizza dove necessario all'interno di un'applicazione web
Link copiato negli appunti

Lo scroll è sempre stato uno degli aspetti più difficili da gestire nelle applicazioni web. Per anni l'unica opzione era ascoltare l'evento scroll, fare calcoli su scrollTop e getBoundingClientRect, e guidare animazioni da JavaScript nel modo più performante possibile — il che significava comunque operare sul main thread, con tutti i rischi di jank che ne conseguono.

Negli ultimi anni il browser ha recuperato terreno in modo significativo: CSS Scroll Snap porta il comportamento di aggancio direttamente nel compositor, e le Scroll-Driven Animations permettono di collegare una timeline di animazione alla posizione di scroll senza una singola riga di JavaScript. Il punto interessante, però, è capire dove e perché JavaScript torna utile anche in questo nuovo scenario.

CSS Scroll Snap: solido, ma non autosufficiente

Scroll Snap è una di quelle API CSS che sembrano semplici in superficie e lo sono davvero per i casi base. Si definisce uno snap container con scroll-snap-type e si marcano gli elementi figli con scroll-snap-align, e il browser gestisce l'aggancio al termine dello scroll in modo nativo, fluido, senza JavaScript. Il risultato è un carosello, uno slideshow a schermo intero, o una galleria orizzontale che si comporta esattamente come ci si aspetta su qualsiasi dispositivo.

.carosello {
  display: flex;
  overflow-x: scroll;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  gap: 1rem;
}

.slide {
  flex: 0 0 100%;
  scroll-snap-align: start;
}

Il problema emerge nel momento in cui si vuole costruire qualcosa di più articolato: navigazione tramite pulsanti, indicatori di slide attiva, sincronizzazione con altri elementi della UI, o logica che deve sapere su quale slide si trova l'utente in un dato momento. Il CSS non espone nulla di tutto questo. È qui che JavaScript rientra, non per guidare lo scroll, ma per osservarlo e reagire.

Rilevare la slide attiva con l'Intersection Observer

Il modo più efficiente per sapere quale elemento è attualmente snappato è usare IntersectionObserver. Configurato con una soglia alta — di solito 0.9 o 1.0 — su elementi che occupano il 100% del container, segnala con precisione quale slide è pienamente visibile. È un approccio molto più pulito dell'alternativa basata sull'evento scroll e sui calcoli di offset, e gira su un thread separato senza impattare le performance.

const slides = document.querySelectorAll('.slide');
let slideAttiva = 0;

const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      slideAttiva = [...slides].indexOf(entry.target);
      aggiornaIndicatori(slideAttiva);
    }
  });
}, {
  root: document.querySelector('.carosello'),
  threshold: 0.9
});
slides.forEach(slide => observer.observe(slide);

Con questa informazione si può aggiornare una serie di dot indicator, sincronizzare un contatore visivo, o abilitare e disabilitare i pulsanti di navigazione precedente e successivo in base alla posizione corrente.

Per navigare tra slide via JavaScript il metodo più affidabile è scrollIntoView con behavior: 'smooth', oppure impostare direttamente scrollLeft sul container. La seconda opzione è più prevedibile quando si conosce la geometria esatta degli elementi, ed è quella da preferire quando si implementano pulsanti avanti e indietro.

function vaiASlide(indice) {
  const carosello = document.querySelector('.carosello');
  const larghezzaSlide = carosello.offsetWidth;
  carosello.scrollTo({
    left: larghezzaSlide * indice,
    behavior: 'smooth'
  });
}

document.querySelector('.btn-avanti').addEventListener('click', () => {
  vaiASlide(Math.min(slideAttiva + 1, slides.length - 1));
});
document.querySelector('.btn-indietro').addEventListener('click', () => {
  vaiASlide(Math.max(slideAttiva - 1, 0));
});

Vale la pena notare che scroll-behavior: smooth sul container CSS e behavior: 'smooth' nell'opzione di scrollTo coesistono senza conflitti. Il CSS definisce il comportamento predefinito, JavaScript può sovrascriverlo puntualmente passando behavior: 'instant' quando serve uno spostamento immediato, per esempio quando l'utente ridimensiona la finestra e si vuole riallineare al punto di snap corretto senza animazione.

Scroll-Driven Animations: animare senza toccare il main thread

Le Scroll-Driven Animations sono una delle aggiunte più significative al CSS degli ultimi anni. Invece di usare il tempo come asse di progresso di un'animazione WAAPI, si usa la posizione di scroll. L'animazione avanza quando l'utente scrolla verso il basso, regredisce quando scrolla verso l'alto, e il tutto avviene interamente nel compositor, senza JavaScript, senza listener, senza jank.

Il meccanismo si basa su due nuovi tipi di timeline: ScrollTimeline, che lega il progresso dell'animazione allo scroll di un elemento, e ViewTimeline, che lo lega all'ingresso e all'uscita di un elemento dal viewport. Entrambe si usano sia in CSS tramite animation-timeline, sia direttamente in JavaScript come oggetto da passare alla WAAPI.

/* CSS puro: la barra di progresso si riempie con lo scroll della pagina */
@keyframes riempi {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
.barra-progresso {
  transform-origin: left;
  animation: riempi linear;
  animation-timeline: scroll(root block);
}

Con scroll(root block) si indica che la timeline è lo scroll verticale del documento. Il browser calcola il progresso dell'animazione come percentuale della posizione di scroll rispetto al massimo scrollabile, e lo applica in tempo reale senza passare per JavaScript.

ViewTimeline: animazioni legate alla visibilità degli elementi

ViewTimeline è il caso d'uso forse più frequente: animare un elemento mentre entra nel viewport. È il pattern "scroll reveal" che per anni ha richiesto librerie come AOS o ScrollMagic, ora fattibile in CSS puro con zero dipendenze.

.card {
  animation: entraInScena linear both;
  animation-timeline: view();
  animation-range: entry 0% entry 40%;
}

@keyframes entraInScena {
  from {
    opacity: 0;
    transform: translateY(40px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

La proprietà animation-range definisce in quale porzione della ViewTimeline l'animazione è attiva. entry 0% entry 40% significa: inizia quando il bordo superiore dell'elemento entra nel viewport, e completati quando il 40% dell'elemento è visibile. I valori possibili includono entry, exit, contain e cover, ognuno con una semantica precisa rispetto alla posizione dell'elemento rispetto allo scrollport.

Controllare ScrollTimeline e ViewTimeline via JavaScript

Quando la logica da applicare è troppo complessa per il CSS — per esempio animazioni condizionali, timeline che dipendono da dati dinamici, o coordinamento tra più elementi — si possono costruire le stesse timeline direttamente in JavaScript e passarle alla WAAPI come si farebbe con qualsiasi altra timeline.

const elemento = document.querySelector('.elemento-animato');

// ViewTimeline: l'animazione è guidata dall'ingresso nel viewport
const timeline = new ViewTimeline({
  subject: elemento,
  axis: 'block'
});
elemento.animate(
  [
    { opacity: 0, transform: 'scale(0.85)' },
    { opacity: 1, transform: 'scale(1)' }
  ],
  {
    timeline,
    rangeStart: 'entry 0%',
    rangeEnd: 'entry 60%',
    fill: 'both'
  }
);

Il vantaggio di questo approccio rispetto al CSS puro è che le keyframes e i parametri dell'animazione possono essere calcolati a runtime. Si può costruire una funzione che genera animazioni personalizzate per ciascun elemento di una lista in base ai dati che quell'elemento rappresenta, o modificare la rangeStart e rangeEnd dinamicamente in risposta a breakpoint o preferenze utente.

Leggere il progresso della timeline in JavaScript

Qualche volta serve conoscere il valore numerico del progresso di scroll non per animare qualcosa, ma per guidare logica applicativa: caricare contenuto al momento giusto, aggiornare lo stato di un componente, sincronizzare elementi fuori dal flusso dell'animazione. L'oggetto Animation restituito da element.animate() espone la proprietà currentTime anche quando la timeline è una ScrollTimeline. Il suo valore cambia con lo scroll, e si può leggere in qualsiasi momento senza listener.

const scrollTimeline = new ScrollTimeline({
  source: document.scrollingElement,
  axis: 'block'
});

// Animazione "fantasma" usata solo per leggere il progresso
const tracker = document.body.animate([], { timeline: scrollTimeline });
function leggiProgresso() {
  // currentTime va da 0 a 100 (come percentuale della timeline)
  const progresso = tracker.currentTime;
  console.log(`Progresso scroll: ${progresso?.toFixed(1)}%`);
}

document.addEventListener('scroll', leggiProgresso, { passive: true });

Questo pattern — un'animazione vuota usata come sonda per leggere il valore della timeline — è meno conosciuto ma molto utile. Evita di ricalcolare manualmente la percentuale di scroll e rimane coerente con qualsiasi ScrollTimeline personalizzata, indipendentemente dal container o dall'asse scelto.

Ti consigliamo anche