Transizione
Accompagna un cambiamento importante della pagina: una modale che si apre, un pannello laterale che entra da fuori schermo.
Guida interattiva a
Transition, keyframes, scroll e View Transitions
Quando qualcosa cambia in una pagina — un menu si apre, un bottone reagisce, una notifica compare — il browser ha due strade: mostrare il risultato finale di colpo, oppure far vedere il cambiamento mentre succede.
La differenza sembra piccola, ma si sente. Senza movimento, ogni modifica arriva dal nulla e l'occhio deve cercare cosa è diverso. Con un breve passaggio animato, il cambiamento si racconta da solo.
Animare serve a rendere l'interfaccia più leggibile.
Lo stato finale arriva di colpo.
Il percorso resta visibile.
Prima di scrivere transition o animation, vale la pena chiedersi che ruolo stia giocando il movimento. Quasi sempre ricade in una di queste cinque categorie.
Accompagna un cambiamento importante della pagina: una modale che si apre, un pannello laterale che entra da fuori schermo.
Un tooltip che compare al passaggio, una notifica leggera che scivola dentro. Mostra un dettaglio senza interrompere il compito principale.
Un bottone che si abbassa al click, un cuoricino che pulsa quando viene attivato. L'interfaccia vi dice: ricevuto.
Serve a spiegare come funziona qualcosa: un breve tutorial animato, un'icona che si trasforma per indicare uno stato.
Tono, personalità, ritmo visivo. Va usata con misura: troppa decorazione distrae dal contenuto.
Prima di scegliere durata, easing e proprietà, conviene riconoscere quale di questi cinque ruoli sta avendo il vostro movimento. Vi aiuta a fare scelte più mirate.
Le animazioni sono un miglioramento (in inglese si dice enhancement), non un requisito. La pagina deve restare leggibile e utilizzabile anche se le animazioni non partono: perché l'utente le ha disattivate, perché il browser non supporta una feature moderna, perché siamo sul primo render prima che il JavaScript abbia caricato.
Concretamente, lo stato finale di un'animazione deve essere comunque visibile e raggiungibile, non nascosto in attesa che il movimento "lo riveli".
Alcune persone sono sensibili al movimento. Animazioni ampie, rotazioni, zoom e parallax possono causare disorientamento, vertigini o nausea. Più avanti useremo prefers-reduced-motion per rispettare questa preferenza, ma il principio vale già da ora: si parte da una versione stabile e si aggiunge il movimento sopra.
Il messaggio è leggibile senza attendere un'animazione.
Stesso contenuto, con una breve entrata dal basso.
Una transition (transizione) è un'istruzione per il browser: invece di passare di colpo da un valore CSS a un altro, calcola i valori intermedi e disegna il cambiamento nel tempo. Questa interpolazione automatica si chiama, appunto, transizione.
Senza transition, il browser applica il nuovo valore istantaneamente:
.button:hover {
transform: translateY(-8px);
}
Il bottone salta nella nuova posizione nel fotogramma successivo all'hover.
Con transition, il browser distribuisce il cambiamento nel tempo:
.button {
transition: transform 250ms;
}
.button:hover {
transform: translateY(-8px);
}
Per 250 millisecondi il transform viene interpolato fotogramma per fotogramma. Lo stato di arrivo è lo stesso, ma il percorso diventa visibile.
La regola transition non descrive un'animazione completa: dichiara come reagire quando un valore CSS cambia. La causa del cambiamento — hover, focus, una classe aggiunta da JavaScript — resta fuori dalla regola.
.button {
transition: transform 250ms;
}
.button:hover {
transform: translateY(-8px);
}
transitionLa proprietà transition è uno shorthand: una scorciatoia che mette più informazioni nella stessa dichiarazione. Prima di usarla con sicurezza, conviene vedere le singole componenti.
Una transizione risponde a quattro domande:
transition-property: quale proprietà CSS deve cambiare nel tempo. Può essere una singola proprietà, una lista separata da virgole o all.transition-duration: quanto dura il passaggio. Si scrive in secondi (0.3s) o millisecondi (300ms).transition-timing-function: con che ritmo avanza la transizione. È la parte che cambia il feeling del movimento.transition-delay: quanto tempo aspetta prima di partire. Il valore iniziale è 0s.Scritta per esteso, una transizione può apparire così:
.card {
transition-property: transform;
transition-duration: 300ms;
transition-timing-function: ease-in-out;
transition-delay: 100ms;
}
.card:hover {
transform: translateY(-8px);
}
Con lo shorthand, le stesse informazioni stanno in una riga:
.card {
transition: transform 300ms ease-in-out 100ms;
}
L'ordine più leggibile è: property, duration, timing function, delay. Quando in una riga compaiono due tempi, il primo è la durata e il secondo è il delay.
Passate sopra la card: aspetta 100 ms, poi si solleva.
Per definire una transition servono almeno due informazioni:
.card {
transition: transform 250ms;
}
.card:hover {
transform: scale(1.04);
}
Possiamo animare più proprietà nello stesso elemento separandole con la virgola. Ogni proprietà può avere durata e ritmo propri:
.card {
transition:
transform 250ms,
opacity 250ms;
}
.card:hover {
transform: translateY(-6px);
opacity: 0.4;
}
Elencate le proprietà nello stesso ordine in cui le dichiarate nel selettore di destinazione. Vi semplifica la rilettura e aiuta chi entrerà nel progetto dopo di voi.
Lo stesso markup cambia comportamento a seconda delle proprietà animate.
.card {
transition: transform 250ms;
}
.card:hover {
transform: translateY(-6px);
}
transition: allIl valore speciale all chiede al browser di animare qualunque proprietà cambi:
.card {
transition: all 250ms;
}
Funziona, e all'inizio sembra il modo più rapido per non doversi ricordare i nomi delle proprietà. Il problema si vede più avanti: appena aggiungete o modificate width, height, margin, color, background, il browser comincia ad animare anche quelle, senza che ve ne accorgiate.
transition: all sembra risparmiare tempo oggi, ma produce animazioni non volute domani, e spesso peggiora le performance perché coinvolge anche proprietà costose come layout o paint.
Vale la pena dichiarare solo le proprietà che intendete davvero animare:
.card {
transition:
transform 250ms,
box-shadow 250ms;
}
Se in futuro ne vorrete animare un'altra, la aggiungete esplicitamente: meglio una riga in più che un comportamento a sorpresa.
Per rendere il confronto leggibile in aula, nella preview la durata è rallentata a 2000 ms: così si vede che nella versione esplicita il colore cambia subito, mentre movimento e ombra continuano ad animarsi.
allAnche il colore del testo viene animato.
Si muovono solo transform e ombra.
La timing function (funzione di temporizzazione) descrive come la transition distribuisce il cambiamento all'interno della propria durata.
Il tempo totale resta lo stesso, ma il ritmo cambia: un movimento può iniziare lento e finire veloce, scattare in avanti e poi rallentare, oppure mantenere un'andatura costante.
.ball {
transition: transform 800ms ease-out;
}
| Valore | Modello mentale | Uso tipico |
|---|---|---|
linear |
velocità costante | spinner, barre di progresso, movimento "tecnico" |
ease (default) |
parte e finisce morbido | casi generici |
ease-in |
parte lenta, accelera in fondo | uscita di un elemento dalla scena |
ease-out |
parte veloce, rallenta in fondo | entrata di un elemento in scena |
ease-in-out |
accelera all'inizio, rallenta alla fine | movimenti avanti-indietro, loop |
Se nessuna delle parole chiave restituisce la sensazione giusta, esiste cubic-bezier(...), che permette di disegnare una curva personalizzata. Per ora basta sapere che esiste: nella maggior parte dei casi ease-out ed ease-in-out coprono i bisogni quotidiani.
Non esiste una durata "perfetta", ma molti micro-movimenti di interfaccia funzionano bene tra 200ms e 500ms.
.button {
transition: transform 180ms ease-out;
}
.modal {
transition:
opacity 300ms ease-out,
transform 400ms ease-out;
}
Se l'utente deve aspettare la fine dell'animazione per continuare a usare l'interfaccia, la durata deve restare breve. Un'animazione bella ma lenta diventa rapidamente fastidiosa, soprattutto quando viene ripetuta più volte in pochi secondi.
.button {
transition: transform 250ms ease-out;
}
transition-delay aggiunge un'attesa prima che la transizione cominci.
.menu {
opacity: 0;
transition: opacity 250ms;
transition-delay: 300ms;
}
.menu-wrapper:hover .menu {
opacity: 1;
transition-delay: 0ms;
}
In questo esempio l'apertura del menu è immediata (transition-delay: 0ms nello stato hover), mentre la chiusura aspetta 300 ms prima di partire. Risultato: se il mouse esce per un istante e rientra subito, il menu non si chiude e riapre nervosamente.
Un piccolo delay sulla "uscita" rende i menu più tolleranti agli errori di mira del mouse.
.menu {
opacity: 0;
transition: opacity 250ms;
transition-delay: 300ms;
}
.menu-wrapper:hover .menu {
opacity: 1;
transition-delay: 0ms;
}
L'entrata e l'uscita di un elemento possono meritare tempi e ritmi diversi. Un trucco classico: scrivere una transition di base sullo stato "a riposo" e una diversa nello stato attivato. Quando lo stato cambia, vale la transition di destinazione.
.button-face {
transform: translateY(0);
transition: transform 400ms ease-out;
}
.button:hover .button-face {
transform: translateY(-4px);
transition: transform 150ms ease-out;
}
Quando il mouse entra, parte la transition rapida (150 ms): il bottone reagisce subito. Quando il mouse esce, vale di nuovo la transition base (400 ms): il bottone ritorna giù in modo più morbido.
A volte conviene rendere l'entrata più rapida dell'uscita, altre volte il contrario: dipende dal tono che state cercando.
Lo stato :active non aspetta: deve sembrare immediato.
.button-face {
transform: translateY(0);
transition: transform 400ms ease-out;
}
.button:hover .button-face {
transform: translateY(-4px);
transition: transform 150ms ease-out;
}
.button:active .button-face {
transform: translateY(2px);
transition-duration: 0ms;
}
Un bug ricorrente nelle micro-interazioni con hover:
.button {
transition: transform 200ms;
}
.button:hover {
transform: translateY(-10px);
}
Sembra innocuo. Su movimenti ampi smette di esserlo. Se il bottone si solleva mentre il mouse è vicino al bordo inferiore, il cursore può ritrovarsi fuori dall'elemento. A quel punto :hover non vale più, il bottone torna giù, il cursore lo intercetta di nuovo, :hover si riattiva, il bottone si solleva di nuovo. È il doom flicker: il flicker maledetto.
La soluzione è separare trigger ed effetto:
<button class="button">
<span class="button-face">Salva</span>
</button>
.button {
padding: 0;
border: 0;
background: transparent;
}
.button-face {
display: block;
padding: 16px 24px;
border: 2px solid currentColor;
background: white;
transition: transform 200ms;
}
.button:hover .button-face {
transform: translateY(-10px);
}
Il <button> resta fermo nella sua posizione e continua a ricevere l'hover. Si muove il <span> interno, ma quello span non è solo un'etichetta: è la faccia visiva completa del bottone, con padding, bordo e sfondo. Il cursore non lo perde mai.
Quando un'animazione sposta un elemento e quell'elemento è anche il trigger dell'hover, applicate il movimento a un figlio.
Il trigger si muove sotto il cursore.
Il trigger resta fermo, si muove la faccia visiva.
transition-property sceglie quale proprietà CSS deve cambiare nel tempo. Il browser può creare una transizione fluida solo quando sa calcolare valori intermedi tra lo stato A e lo stato B.
width, height, font-size, border-radius, opacity, left, topcolor, background-color, border-colortranslate, scale, rotate, dentro transformbox-shadow, text-shadowdisplay: da block a flex cambia a scattofont-family: non esiste un "mezzo font" tra Arial e Georgiaposition: cambia il modello di posizionamentoheight: 0 verso height: auto: auto non è una misura numerica da interpolare*.card {
opacity: 1;
transform: translateY(0);
background-color: white;
transition:
opacity 250ms ease-out,
transform 250ms ease-out,
background-color 250ms ease-out;
}
.card:hover {
opacity: 0.65;
transform: translateY(-8px);
background-color: #ede9fe;
}
.panel {
opacity: 1;
visibility: visible;
transition:
opacity 250ms ease-out,
visibility 0s linear 250ms;
}
.panel.is-hidden {
opacity: 0;
visibility: hidden;
}
transition-behavior: allow-discrete permette di avviare transizioni anche per proprietà discrete. Per display, il comportamento è utile ma non ancora uniforme: funziona in Chromium e Safari, non in Firefox.
*Per auto, una soluzione è usare interpolate-size: allow-keywords, che però è supportato in Chrome ed Edge, non in Safari e Firefox.
Opacity, transform e background-color cambiano gradualmente.
Il layout cambia a scatto: non c'è uno stato intermedio utile.
block flexIl fade è fluido; l'interazione si disattiva alla fine.
Elemento cliccabileAprite CodePen (o l'editor che usate in classe) e create una micro-interazione hover su una griglia di card. La consegna ha tre obiettivi: usare transition su proprietà specifiche, separare il trigger dall'effetto per evitare il doom flicker, e gestire anche il focus da tastiera. Provate prima a leggere "Cosa esplorare" senza aprire la soluzione.
<div class="cards">
<a class="card-link" href="#">
<article class="card">
<h2>Chrome</h2>
<p>Browser per testare animazioni moderne.</p>
</article>
</a>
<a class="card-link" href="#">
<article class="card">
<h2>Safari</h2>
<p>Altro browser target della lezione.</p>
</article>
</a>
</div>
/* Reset minimo */
*, *::before, *::after {
box-sizing: border-box;
padding: 0;
margin: 0;
}
/* Tipografia base */
body {
font-family: sans-serif;
padding: 32px;
background: #eef2ff;
}
/* Layout della griglia */
.cards {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
}
/* Link wrapper: trigger stabile dell'hover */
.card-link {
color: inherit;
text-decoration: none;
}
/* Card: contenuto che si solleva */
.card {
min-height: 180px;
padding: 24px;
border: 2px solid #4f46e5;
border-radius: 8px;
background: white;
}
transition su .card che animi transform e box-shadow..card-link:hover .card, usate transform: translateY(-8px) e una box-shadow più marcata.transform direttamente su .card-link: cosa cambia rispetto al doom flicker?.card-link:focus .card: la card deve sollevarsi anche con il tasto Tab.box-shadow a due strati.Le transition sono ottime quando esistono due stati CSS ben definiti: normale e hover, chiuso e aperto, visibile e nascosto. Il browser fa il resto.
Le keyframe animations servono quando volete descrivere una sequenza che si svolge nel tempo, indipendentemente da uno stato. Per esempio: "all'apertura della pagina, questo titolo entra dall'alto" o "questo badge pulsa tre volte e si ferma".
@keyframes slide-in {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
@keyframes definisce i passaggi e prende un nome (qui slide-in). La proprietà animation applica quei passaggi a un elemento concreto:
.panel {
animation: slide-in 500ms ease-out;
}
@keyframes definisce un movimento riusabile. animation lo applica. I due pezzi possono vivere in punti diversi del foglio di stile.
La sequenza parte quando la classe è applicata.
@keyframes + animationUn'animazione CSS ha due parti che lavorano insieme:
@keyframes pop-in {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
.badge {
animation: pop-in 300ms ease-out;
}
Il nome pop-in collega il blocco @keyframes all'elemento .badge. Le parole chiave from e to sono scorciatoie per 0% e 100%: tornano comode quando l'animazione ha solo punto di partenza e arrivo.
Se due blocchi @keyframes hanno lo stesso nome, l'ultimo dichiarato vince. Il browser non avvisa: il movimento cambia silenziosamente. Usate nomi descrittivi e univoci per progetto.
@keyframes pop-in {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
.badge {
animation: pop-in 300ms ease-out;
}
Dentro un singolo blocco @keyframes possiamo cambiare più proprietà insieme. È il pattern più comune per le "entrate" di un elemento: combinare opacità e una piccola traslazione fa percepire l'arrivo senza essere invadente.
@keyframes drop-in {
from {
opacity: 0;
transform: translateY(-32px) rotate(-8deg);
}
to {
opacity: 1;
transform: translateY(0) rotate(0deg);
}
}
.note {
animation: drop-in 600ms ease-out;
}
Le proprietà più adatte ad animazioni fluide sono transform e opacity. Permettono al browser di lavorare sul livello visivo dell'elemento, senza ricalcolare il layout. Ci torneremo nella parte sulle performance.
Opacità e trasformazione lavorano nello stesso keyframe.
Per default, un'animazione CSS parte una volta sola e si ferma.
animation-iteration-count controlla quante volte ripetere:
.pulse {
animation-name: pulse;
animation-duration: 900ms;
animation-iteration-count: 3;
}
Il valore speciale infinite ripete senza fine:
.spinner {
animation: spin 800ms linear infinite;
}
Le animazioni infinite vanno scelte con cura. Catturano l'attenzione anche quando non dovrebbero, e per chi preferisce ridurre il movimento diventano un ostacolo concreto. Usatele solo dove il loop fa parte del significato.
from e to sono scorciatoie per 0% e 100%. Quando l'animazione ha più passaggi, le percentuali diventano necessarie: ogni percentuale rappresenta un punto della timeline.
@keyframes badge-pop {
0% {
transform: scale(0.8);
opacity: 0;
}
60% {
transform: scale(1.08);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 1;
}
}
In questo esempio, il badge entra ingrandendosi oltre il punto di arrivo (1.08) e poi si "assesta" a scala 1. È il classico overshoot, che dà personalità al movimento senza esagerare.
Le percentuali nei keyframe sono punti di una timeline normalizzata, non secondi. Lo stesso blocco @keyframes può durare 300 ms o 3 secondi: la forma del movimento resta uguale, cambia solo quanto tempo impiega a svolgersi.
animation-direction controlla in che ordine il browser legge i keyframe.
@keyframes breathe {
0% {
transform: scale(1);
}
100% {
transform: scale(1.2);
}
}
.breathing-box {
animation: breathe 1200ms ease-in-out infinite alternate;
}
Con il valore alternate, un ciclo va da 0% a 100%, il successivo torna da 100% a 0%, e così via. Senza alternate, alla fine di ogni ciclo il browser tornerebbe di colpo al punto di partenza.
| Valore | Effetto |
|---|---|
normal |
dal primo all'ultimo keyframe (default) |
reverse |
dall'ultimo al primo |
alternate |
alternato: avanti, indietro, avanti, indietro |
alternate-reverse |
come alternate, ma comincia all'indietro |
Uso tipico: una pulsazione che respira, un indicatore di attesa, un'onda che invita al clic.
animationQuasi tutte le proprietà animation-* possono entrare nello shorthand animation:
.box {
animation: breathe 1200ms ease-in-out infinite alternate;
}
È equivalente a scrivere:
.box {
animation-name: breathe;
animation-duration: 1200ms;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
animation-direction: alternate;
}
Quando entra in gioco anche un delay, conviene scriverlo separato. Due valori di tempo nello stesso shorthand confondono chi rilegge il codice: non è chiaro quale sia la durata e quale il delay. Una riga in più rende il codice immediato.
.toast {
animation: slide-in 400ms ease-out both;
animation-delay: 300ms;
}
.toast {
animation: slide-in 400ms ease-out both;
animation-delay: 300ms;
}
Un caso che capita spesso. Questo codice fa sfumare l'elemento fino a sparire, ma alla fine dell'animazione l'elemento torna visibile:
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
.box {
animation: fade-out 1000ms;
}
L'animazione dura un secondo, l'opacità scende a zero, e poi l'elemento riappare di colpo.
Le dichiarazioni dentro un keyframe valgono solo mentre l'animazione è attiva. Quando finisce, il browser smette di applicarle e torna alle regole CSS normali dell'elemento. Nel nostro caso .box non ha un'opacity dichiarata, quindi vale il default 1: l'elemento ricompare.
Dopo l'animazione, l'elemento torna a essere quello descritto dal CSS normale. Per fissare lo stato finale serve animation-fill-mode.
animation-fill-mode controlla se i valori dei keyframe vengono applicati anche fuori dalla durata dell'animazione: prima che cominci, durante un eventuale delay, o dopo che è finita.
| Valore | Effetto |
|---|---|
none |
i keyframe valgono solo durante l'animazione (default) |
backwards |
durante il delay, applica già i valori del keyframe iniziale |
forwards |
dopo la fine, mantiene i valori del keyframe finale |
both |
combina forwards e backwards |
Per rendere la differenza visibile, usiamo un esempio in cui lo stato CSS normale è volutamente diverso sia dal keyframe iniziale sia da quello finale.
.toast {
background: #f3f4f6;
opacity: 0.35;
transform: translateY(-18px);
animation-name: slide-in;
animation-duration: 700ms;
animation-timing-function: ease-out;
animation-delay: 600ms;
animation-fill-mode: backwards;
}
Con backwards, durante il delay il toast usa già il keyframe iniziale. Con forwards, dopo la fine mantiene il keyframe finale. Con both, applica entrambe le cose.
Per le entrate con delay, both è quasi sempre il valore più semplice da ragionare. Per le uscite, in cui l'elemento sparisce e deve restare sparito, scegliete forwards.
Aprite CodePen e create un help button che entra in scena. La consegna combina tre concetti già visti: @keyframes con from/to, animation-delay, e animation-fill-mode per evitare che il pulsante "torni indietro" dopo l'animazione. Provate prima a leggere "Cosa esplorare" e a scrivere il CSS senza aprire la soluzione.
Un pulsante circolare fisso nell'angolo in basso a destra della pagina, con il punto interrogativo al centro. Dopo circa mezzo secondo entra dal basso con una leggera dissolvenza e resta stabile nella posizione finale.
Create prima il bottone con un nome accessibile. Il testo visibile può essere solo ?, ma il significato completo va dato con aria-label.
<button class="help-button" aria-label="Apri aiuto">
?
</button>
Partite da questo CSS: contiene solo reset, sfondo e aspetto base del pulsante. L'animazione va aggiunta dopo.
/* Reset minimo */
*, *::before, *::after {
box-sizing: border-box;
padding: 0;
margin: 0;
}
/* Sfondo della pagina */
body {
min-height: 100vh;
font-family: sans-serif;
background: #f8fafc;
}
/* Aspetto del pulsante */
.help-button {
width: 64px;
height: 64px;
border: 3px solid white;
border-radius: 50%;
background: #4f46e5;
color: white;
font-size: 2rem;
font-weight: 700;
cursor: pointer;
}
position: fixed, right e bottom.@keyframes slide-in che parta da translateY(120%) e opacity: 0 fino allo stato finale.ease-out.animation-delay: 500ms e provate prima senza fill mode.animation-fill-mode: backwards e infine both: quale risultato è più pulito?Le due famiglie si sovrappongono in alcuni casi e si distinguono in altri. Lo strumento giusto dipende da cosa state raccontando: un passaggio tra stati o una sequenza nel tempo.
| Caso | Strumento consigliato | Perché |
|---|---|---|
| Cambio tra due stati CSS | transition |
Lo stato esiste già nel selettore, basta accompagnarlo |
| Hover, focus, active | transition |
Le pseudo-classi gestiscono lo stato |
| Sequenza con più passaggi | @keyframes |
Le percentuali raccontano la timeline |
| Loop, pulsazioni, spinner | @keyframes |
Supporta ripetizione e direzione |
| Avvio al caricamento | @keyframes |
Parte appena la regola è applicata |
| Animazione guidata dallo scroll | @keyframes + timeline |
I keyframe vengono mappati su un range di scroll |
Per addolcire un cambiamento di stato, partite da transition. Per descrivere una sequenza nel tempo, partite da @keyframes. Se vi accorgete di forzare uno strumento a fare il lavoro dell'altro, cambiate strumento.
animation-play-state può fermare e riprendere un'animazione senza ricrearla.
.loader {
animation: spin 900ms linear infinite;
animation-play-state: running;
}
.loader.is-paused {
animation-play-state: paused;
}
Mini snippet JS per togglare la classe, e quindi lo stato di play:
const button = document.querySelector(".pause-button");
const loader = document.querySelector(".loader");
button.addEventListener("click", function () {
loader.classList.toggle("is-paused");
});
Il bottone congela l'animazione e la fa ripartire dal punto in cui era. Se serve far ripartire l'animazione dall'inizio, va rimossa e riaggiunta la classe che la attiva.
In CSS pensiamo a stati: chiuso o aperto, spento o acceso, inattivo o attivo. L'utente percepisce azioni: sto aprendo, sto chiudendo, sto premendo, ho appena rilasciato.
Le due azioni opposte non meritano lo stesso ritmo. Aprire spesso vuole gradualità per dare tempo agli occhi di trovare il nuovo contenuto. Chiudere può essere più rapido: l'utente ha già deciso, non serve trattenerlo.
.panel {
opacity: 0;
transform: translateY(16px);
transition:
opacity 180ms ease-in,
transform 180ms ease-in;
}
.panel.is-open {
opacity: 1;
transform: translateY(0);
transition:
opacity 360ms ease-out,
transform 360ms ease-out;
}
La transition nello stato .is-open decide il ritmo dell'apertura. Quando la classe .is-open viene rimossa, vale di nuovo la transition base, che decide il ritmo della chiusura.
Il pannello entra con calma e si chiude più in fretta.
Orchestrare significa distribuire più elementi animati lungo una stessa timeline, in modo che si succedano invece di sovrapporsi tutti insieme.
Esempio classico: una modale. Il backdrop sfuma subito, il pannello entra leggermente dopo, il bottone di chiusura compare per ultimo.
.backdrop {
transition: opacity 300ms ease-out;
}
.dialog {
transition: transform 350ms ease-out;
transition-delay: 120ms;
}
.close-button {
transition: opacity 180ms ease-out;
transition-delay: 300ms;
}
Decidere l'ordine di entrata e i tempi di attesa fra un elemento e l'altro è il lavoro principale dell'orchestrazione: l'utente non vede i delay, ma percepisce una sequenza ordinata.
Sequenza: sfondo, pannello, chiusura.
Perché un'animazione sembri fluida, il browser deve aggiornarla molte volte al secondo. A 60 frame al secondo ha circa 16 millisecondi per preparare ogni frame: se ci mette di più, il movimento appare a scatti.
Quando potete, animate sul Compositing ed evitate le proprietà che richiedono Layout o Paint.
Proprietà spesso costose:
height
width
margin
padding
top
left
Proprietà spesso più adatte all'animazione:
transform
opacity
Due modi diversi per ottenere lo stesso movimento visivo:
.box-a:hover {
margin-top: 12px;
}
.box-b:hover {
transform: translateY(12px);
}
La prima versione cambia il margin-top reale dell'elemento: il browser ricalcola il layout della pagina, perché spostare quella box può influenzare la posizione degli elementi vicini.
La seconda versione lavora sul livello visivo dell'elemento: lo trasla senza modificarne la posizione nel layout. Il movimento è quasi sempre più fluido.
will-change è un suggerimento al browser: "questa proprietà sta per cambiare, preparati":
.card {
will-change: transform;
}
Non aggiungete will-change ovunque per stare tranquilli. Consuma memoria e può peggiorare le performance se applicato a troppi elementi. Usatelo solo dove sapete che l'animazione partirà davvero.
margin-toptransformLa media query prefers-reduced-motion permette di sapere se l'utente ha chiesto al sistema operativo di ridurre il movimento delle interfacce.
Il pattern consigliato è partire da una versione stabile e priva di animazioni, poi abilitare il movimento solo dentro no-preference.
.card {
transform: translateY(0);
}
.card:hover {
transform: translateY(-8px);
}
@media (prefers-reduced-motion: no-preference) {
.card {
transition: transform 220ms ease-out;
}
}
In questo modo, chi ha attivato la riduzione del movimento riceve comunque il cambio di stato, ma senza la transizione animata.
Non tutte le animazioni vanno spente. Un piccolo cambio di colore o uno scale di pochi pixel sono molto diversi da una rotazione ampia o da un parallax a tutta pagina. Valutate caso per caso.
Passate sopra la card: il cambio di stato resta disponibile anche quando la transizione viene ridotta.
Risultato atteso: una card di conferma centrata in pagina. Con preferenza no-preference entra dal basso con una leggera dissolvenza. Con movimento ridotto, la card è già visibile in posizione: nessuno spostamento, al massimo un fade leggerissimo.
Il vostro profilo è stato aggiornato.
Aprite CodePen e adattate la card alla preferenza dell'utente. Il vincolo è uno: la versione base deve essere quella senza movimento, e l'animazione si aggiunge sopra dentro @media (prefers-reduced-motion: no-preference). Se non potete cambiare la preferenza nel sistema operativo, simulate l'effetto commentando temporaneamente la media query.
<article class="notice">
<h2>Operazione completata</h2>
<p>Il vostro profilo è stato aggiornato.</p>
</article>
/* Reset minimo */
*, *::before, *::after {
box-sizing: border-box;
padding: 0;
margin: 0;
}
/* Layout della pagina */
body {
min-height: 100vh;
display: grid;
place-content: center;
font-family: sans-serif;
background: #f1f5f9;
}
/* Aspetto della card */
.notice {
max-width: 360px;
padding: 24px;
border: 2px solid #0f766e;
border-radius: 8px;
background: white;
}
@keyframes enter che parta da opacity: 0 e transform: translateY(24px) e arrivi allo stato visibile.animation: enter 400ms ease-out both solo dentro @media (prefers-reduced-motion: no-preference).opacity e nessun movimento.Finora le animazioni avanzavano nel tempo: 800 ms di durata significavano 800 ms di movimento, indipendentemente da cosa stesse facendo l'utente.
.box {
animation: fade-in 1000ms;
}
Con le scroll-driven animations, l'avanzamento dei keyframe dipende dalla posizione dello scroll.
La posizione dell'elemento dentro la viewport diventa la manopola che fa avanzare o indietreggiare l'animazione. Se l'utente smette di scrollare, l'animazione si ferma esattamente dov'è.
Attenzione a una distinzione spesso confusa: scroll-driven non significa scroll-triggered. Un'animazione triggered parte quando l'elemento entra in scena, ma poi prosegue da sola sul tempo. Una scroll-driven è guidata, frame per frame, dallo scroll.
Queste API sono moderne. Le presentiamo come progressive enhancement: se il browser non le supporta, il contenuto deve restare visibile e usabile senza animazione.
Parte da sola e avanza anche se non fate nulla.
Scorrete il riquadro: il progresso dipende dalla posizione.
Il prossimo blocco fa sfumare un elemento da trasparente a opaco man mano che attraversa la viewport, senza un solo listener di scroll:
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.reveal {
animation: fade-in linear both;
animation-timeline: view();
animation-range: entry; /* Lo spieghiamo nella prossima slide. */
}
view() chiede al browser di creare una timeline collegata al passaggio dell'elemento dentro la viewport, o dentro il contenitore scrollabile più vicino se l'elemento sta dentro a un overflow: auto.
Quando l'elemento è sotto la viewport, l'animazione è al keyframe 0. Mentre l'elemento sale, l'animazione avanza. Quando ha attraversato la viewport, è al keyframe 100%.
In questo esempio compare già animation-range: entry: lo vediamo in dettaglio nella prossima slide, ma qui serve a limitare il fade alla fase in cui la card entra nel riquadro.
Nello shorthand animation non si può inserire animation-timeline. Dichiaratela su una riga separata, dopo lo shorthand, altrimenti rischiate di sovrascriverla.
animation-range: la parte di scroll che contaanimation-range controlla quale parte del passaggio nella viewport deve guidare l'animazione. Senza specificarlo, l'animazione copre l'intero attraversamento, cioè cover, che spesso è troppo lungo.
.reveal {
animation: slide-in linear both;
animation-timeline: view();
animation-range: entry;
}
| Valore | Significato pratico |
|---|---|
entry |
l'animazione si svolge mentre l'elemento sta entrando in viewport |
contain |
l'animazione avanza solo mentre l'elemento è completamente visibile |
cover |
l'animazione copre l'intero attraversamento, ed è il default |
exit |
l'animazione si svolge mentre l'elemento sta uscendo |
Per il classico effetto reveal on scroll, entry è il punto di partenza ragionevole. Se la card si muove ancora quando è già ben visibile, probabilmente state usando cover.
entrycoverRisultato atteso: una lista di card in una pagina abbastanza alta da richiedere scroll. Ogni card, mentre entra nella viewport, sfuma e si sposta leggermente verso l'alto. Se il browser non supporta animation-timeline, le card restano visibili senza animazione.
Aprite CodePen e create un effetto reveal on scroll usando solo CSS, senza JavaScript. La consegna combina @keyframes, animation-timeline: view() e animation-range: entry. Il vincolo più importante: deve esserci un fallback visivo.
<main class="timeline">
<article class="reveal">Primo contenuto</article>
<article class="reveal">Secondo contenuto</article>
<article class="reveal">Terzo contenuto</article>
</main>
/* Reset minimo */
*, *::before, *::after {
box-sizing: border-box;
padding: 0;
margin: 0;
}
/* Tipografia base */
body {
font-family: sans-serif;
background: #0f172a;
color: white;
}
/* Contenitore della lista */
.timeline {
width: min(680px, calc(100% - 32px));
margin-left: auto;
margin-right: auto;
padding-top: 80vh;
padding-bottom: 80vh;
}
/* Aspetto delle card: stato visibile di default */
.reveal {
min-height: 180px;
margin-bottom: 32px;
padding: 24px;
border-radius: 8px;
background: #334155;
}
@keyframes reveal che parta da opacity: 0 e transform: translateY(40px)..reveal con animation: reveal linear both.animation-timeline: view() su riga separata.animation-range: entry e confrontatelo con cover.@media (prefers-reduced-motion: no-preference) attorno alle regole di animazione.Le View Transitions permettono al browser di animare il passaggio tra due viste della pagina: due schermate, due rotte, due documenti diversi.
Nei progetti Single Page Application, o SPA, si usa JavaScript con document.startViewTransition(). In questa lezione ci interessa il caso più vicino ai vostri progetti HTML/CSS: una Multi Page Application, o MPA, cioè un sito composto da più file HTML separati.
Con le MPA moderne possiamo abilitare le view transitions interamente in CSS:
@view-transition {
navigation: auto;
}
Inserendo questa regola in entrambe le pagine, ogni navigazione interna allo stesso sito viene accompagnata da una transizione visiva.
Una view transition non sostituisce le animazioni interne alla pagina. È un livello aggiuntivo: il browser cattura la vecchia vista e la nuova vista, poi le fonde insieme.
È una feature moderna. Non è un requisito per far funzionare il sito: se il browser non la supporta, il link cambia pagina normalmente, senza transizione.
Perché una transizione tra due documenti funzioni, servono condizioni semplici:
@view-transition { navigation: auto; };@view-transition {
navigation: auto;
}
Niente JavaScript obbligatorio per il caso MPA base. Il browser si occupa di tutto.
Durante una view transition, il browser crea due pseudo-elementi che rappresentano la vecchia vista (::view-transition-old) e la nuova vista (::view-transition-new). Potete intercettarli con CSS e dare loro animazioni dedicate.
La demo esterna in public/demos/view-transition/ usa proprio questa personalizzazione: due pagine reali, index.html e dettagli.html, condividono lo stesso styles.css. Apritela e navigate tra elenco e dettaglio per vedere il browser applicare il codice qui sotto.
Questo è il blocco relativo alle View Transitions caricato dalla demo demos/view-transition/index.html tramite il suo styles.css.
@view-transition {
navigation: auto;
}
::view-transition-old(root) {
animation: page-out 300ms ease-in both;
}
::view-transition-new(root) {
animation: page-in 300ms ease-out both;
}
@keyframes page-out {
to {
opacity: 0;
transform: translateY(-16px);
}
}
@keyframes page-in {
from {
opacity: 0;
transform: translateY(16px);
}
}
@media (prefers-reduced-motion: reduce) {
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
}
}
In questo esempio, la vecchia pagina sfuma verso l'alto e la nuova pagina entra dal basso. Il risultato è una transizione coerente, verticale, tra le due viste.
Partite dalla transizione di default del browser, un fade incrociato. Personalizzatela solo se il movimento aiuta l'utente a orientarsi.
| Concetto | Quando usarlo | Da ricordare |
|---|---|---|
transition |
cambio tra stati CSS | dichiarare proprietà specifiche, non all |
| timing function | controllare il ritmo | il tempo totale resta uguale |
transition-delay |
ritardare entrata o uscita | spesso meglio separato dallo shorthand |
@keyframes |
sequenze, loop, avvio al caricamento | scegliere nomi descrittivi e univoci |
animation-fill-mode |
valori prima e dopo l'animazione | both è spesso sensato per le entrate |
transform / opacity |
movimento fluido | lavorano sul livello visivo, niente layout |
will-change |
preparare il browser a un'animazione | usarlo solo dove serve davvero |
prefers-reduced-motion |
accessibilità | partire da una versione stabile |
animation-timeline: view() |
animazioni guidate dallo scroll | enhancement moderno, sempre con fallback |
animation-range |
quando inizia e finisce l'animazione | entry per i classici reveal |
@view-transition |
transizioni tra pagine MPA | enhancement moderno, niente JS obbligatorio |
Animare significa scegliere ogni volta lo strumento CSS giusto per il problema di interfaccia che avete davanti.
Costruite una piccola pagina di presentazione con micro-interazioni progressive. Ogni step deve funzionare anche se non completate i successivi. Partite da un layout di base con titolo, hero e griglia di card, poi aggiungete movimento, accessibilità ed effetti moderni.
Pensate a una landing page semplice: hero in alto, lista di card più sotto, CTA fluttuante in basso a destra ed eventuale pagina di dettaglio raggiungibile cliccando su una card. Lo stato finale deve restare leggibile anche senza animazioni.
:hover e su :focus, sollevate il contenuto interno con transform: translateY(...).transition breve su transform e box-shadow.fixed.@keyframes, combinando opacity e transform.animation-fill-mode: both per evitare il bug del ritorno.@media (prefers-reduced-motion: no-preference).animation-timeline: view() e animation-range: entry.dettagli.html.@view-transition { navigation: auto; } in entrambe.::view-transition-old(root) e ::view-transition-new(root) con un movimento sobrio.:hover e :focus sono coperti entrambi.transform e opacity.Grazie.
Fonti: Josh W. Comeau e MDN Web Docs su CSS Animations, Scroll-driven animations e View Transitions API.