Chi lavora con le animazioni web da un po' conosce bene la sensazione di scegliere lo strumento sbagliato a metà progetto. Si comincia con una semplice transition CSS, poi arriva il requisito di controllarla dinamicamente via JavaScript, e ci si ritrova ad aggiungere classi a runtime, ascoltare transitionend, e gestire stati con logica sempre più fragile. Oppure si parte da un'animazione CSS con keyframes elaborata, e quando serve metterla in pausa, invertirla o sincronizzarla con un'altra, il CSS da solo non basta più. Vale la pena fermarsi a capire cosa fa davvero bene ciascun approccio, e dove invece inizia a scricchiolare.
CSS Transitions per le animazioni: eleganti, ma con un perimetro preciso
Le CSS Transitions sono nate per interpolare lo stato di una proprietà CSS tra due valori quando quella proprietà cambia. Niente di più, niente di meno. Definisci quale proprietà osservare, quanto deve durare la transizione, con quale easing, e il browser si occupa del resto. Il modello mentale è reattivo: la transizione si attiva in risposta a un cambiamento di stato, che sia un hover, una classe aggiunta dal JavaScript, o una media query che scatta.
.bottone {
background-color: #3b82f6;
transform: scale(1);
transition: background-color 300ms ease, transform 200ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.bottone:hover {
background-color: #1d4ed8;
transform: scale(1.05);
}
Il browser gestisce queste transizioni sul compositor thread quando le proprietà animate sono transform e opacity, il che significa nessun reflow, nessun repaint, animazione a 60fps anche su dispositivi mediocri. Questo è il vantaggio più concreto delle transitions CSS: quando si animano le proprietà giuste, il costo di rendering è minimo per definizione architetturale, non per ottimizzazione manuale.
Il limite emerge non appena si vuole fare qualcosa di più: mettere in pausa una transizione a metà percorso, sapere esattamente a che punto si trova, o controllarne la velocità dinamicamente sono tutte cose che le transitions non supportano. L'unico evento esposto è transitionend, che ti dice solo quando è finita. Per tutto il resto, sei fuori dal perimetro.
CSS Keyframes: potenza dichiarativa con controllo limitato
Le animazioni con @keyframes alzano l'asticella: invece di interpolare tra due stati, permettono di definire una sequenza arbitraria di stati distribuiti nel tempo. Si descrive il comportamento completo dell'animazione in CSS, con pieno controllo sugli step intermedi, e la si collega a un elemento tramite la proprietà animation. Il risultato è spesso molto espressivo con pochissimo codice.
@keyframes rimbalzo {
0% { transform: translateY(0); animation-timing-function: ease-in; }
45% { transform: translateY(-120px); animation-timing-function: ease-out; }
55% { transform: translateY(-120px); animation-timing-function: ease-in; }
100% { transform: translateY(0); }
}
.pallina {
animation: rimbalzo 800ms infinite alternate;
}
La possibilità di specificare un easing diverso per ogni segmento della timeline è uno dei dettagli meno celebrati delle keyframes CSS, ma fa una differenza enorme nel risultato finale. Un rimbalzo fisicamente credibile ha una curva di accelerazione in discesa e una di decelerazione in risalita: gestirlo segmento per segmento è preciso e leggibile.
Dove le keyframes mostrano i denti è nell'interazione con JavaScript. Le proprietà dell'animazione si possono leggere e scrivere tramite il CSSOM, ma l'interfaccia è goffa. Mettere in pausa si fa con animation-play-state: paused, che funziona. Sapere a che percentuale della timeline si trova l'animazione in un dato momento non è direttamente possibile senza calcoli manuali basati sul tempo trascorso. Reagire alla fine di un ciclo specifico richiede di ascoltare animationiteration e contare.
Non è impossibile, ma si sente il peso dell'approccio dichiarativo quando le esigenze diventano imperative.
Il problema del gap tra CSS e JavaScript
Entrambi gli approcci CSS soffrono dello stesso problema strutturale: vivono in un sistema dichiarativo, ma molte animazioni richiedono controllo imperativo. Quando si costruisce un'interfaccia con animazioni coordinate — elementi che entrano in scena in sequenza, animazioni che rispondono a input utente in tempo reale, sequenze che si concatenano dinamicamente — si finisce per costruire una macchina a stati in JavaScript che coordina classi CSS, ascolta eventi di fine animazione, gestisce edge case sui tempi. Il codice funziona, ma è fragile e difficile da mantenere.
Web Animations API: il ponte tra i due mondi
La Web Animations API, nota come WAAPI, è stata progettata esattamente per colmare questo gap. Porta il modello delle keyframes CSS direttamente in JavaScript, aggiungendo un'interfaccia di controllo ricca e una timeline programmabile. Non è una libreria: è una API nativa del browser, ben supportata da Chrome 84+, Firefox 63+ e Safari 13.1+.
Il metodo di ingresso è element.animate(), che accetta un array di keyframes e un oggetto di opzioni. La sintassi è deliberatamente familiare a chiunque conosca le keyframes CSS.
const animazione = document.querySelector('.box').animate(
[
{ transform: 'translateX(0px)', opacity: 1 },
{ transform: 'translateX(300px)', opacity: 0.5, offset: 0.7 },
{ transform: 'translateX(500px)', opacity: 0 }
],
{
duration: 1000,
easing: 'cubic-bezier(0.25, 0.1, 0.25, 1)',
fill: 'forwards',
iterations: 1
}
);
La proprietà offset nell'array delle keyframes corrisponde esattamente alle percentuali dei @keyframes CSS: un valore di 0.7 equivale al 70%. Senza offset esplicito, le keyframes vengono distribuite uniformemente.
Controllo imperativo sulla timeline
element.animate() restituisce un oggetto Animation, e qui sta tutta la differenza rispetto al CSS. Su questo oggetto si può chiamare pause(), play(), reverse(), e cancel() in qualsiasi momento. La proprietà currentTime espone la posizione attuale sulla timeline in millisecondi, ed è scrivibile: impostarla a 500 su un'animazione da 1000ms la porta esattamente a metà percorso istantaneamente. La proprietà playbackRate controlla la velocità: impostarla a 2 fa girare l'animazione al doppio della velocità, a -1 la manda in reverse.
const anim = elemento.animate(keyframes, { duration: 1200, fill: 'forwards' });
// Pausa immediata
anim.pause();
// Salta a metà animazione
anim.currentTime = 600;
// Inversione con velocità aumentata
anim.playbackRate = -2;
anim.play();
// Promise che si risolve al completamento
anim.finished.then(() => console.log('Animazione terminata'));
La proprietà finished è una Promise nativa: niente più listener su animationend, niente gestione manuale di callback. Si può usare await anim.finished dentro una funzione asincrona per costruire sequenze di animazioni leggibili come codice procedurale.
async function sequenzaEntrata(elementi) {
for (const el of elementi) {
const anim = el.animate(
[{ opacity: 0, transform: 'translateY(20px)' },
{ opacity: 1, transform: 'translateY(0)' }],
{ duration: 400, easing: 'ease-out', fill: 'forwards' }
);
await anim.finished;
}
}
Ogni elemento aspetta che il precedente abbia finito prima di entrare. La stessa logica con CSS e animationend richiederebbe una catena di listener o un sistema di promise costruito a mano. Qui è tutto lineare, e si legge come si pronuncia.
GroupEffect e SequenceEffect: orchestrazione avanzata
La specifica completa della WAAPI include anche GroupEffect e SequenceEffect, che permettono di raggruppare animazioni in parallelo o in serie come unità atomica, controllabile da un unico oggetto Animation. Il supporto nativo è ancora parziale, ma la libreria web-animations-js offre un polyfill completo per chi ne ha bisogno oggi. Quando il supporto browser sarà universale, orchestrare animazioni complesse senza dipendenze esterne diventerà completamente praticabile.
Quando usare cosa, davvero
Le CSS Transitions restano imbattibili per stati UI semplici — hover, focus, toggle di classi — dove il browser può ottimizzare tutto sul compositor e il CSS vive dove deve stare, nel foglio di stile. Le keyframes CSS hanno senso per animazioni decorative autonome, cicliche o semi-cicliche, che non devono essere controllate a runtime e che si esprimono naturalmente in modo dichiarativo. La WAAPI diventa la scelta giusta non appena serve controllo programmatico: animazioni guidate da dati, sequenze condizionali, interazioni che rispondono a gesti o input in tempo reale, o qualsiasi scenario dove la logica di animazione è intrecciata con la logica applicativa.
La sua forza non è sostituire il CSS, ma evitare di scrivere del codice fragile ogni volta che il CSS non basta.