ITS Web Design e Strategie Digitali
ITS Academy I-CREA @ CFP Bauer

Guida interattiva a

Animazioni CSS

Transition, keyframes, scroll e View Transitions

Adattato da materiali di Josh W. Comeau e MDN

A cosa serve animare

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.

Idea guida

Animare serve a rendere l'interfaccia più leggibile.

Senza movimento

Lo stato finale arriva di colpo.

Card semplice

Con transizione

Il percorso resta visibile.

Card animata

Cinque ruoli del movimento

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.

Transizione

Accompagna un cambiamento importante della pagina: una modale che si apre, un pannello laterale che entra da fuori schermo.

Supplemento

Un tooltip che compare al passaggio, una notifica leggera che scivola dentro. Mostra un dettaglio senza interrompere il compito principale.

Feedback

Un bottone che si abbassa al click, un cuoricino che pulsa quando viene attivato. L'interfaccia vi dice: ricevuto.

Dimostrazione

Serve a spiegare come funziona qualcosa: un breve tutorial animato, un'icona che si trasforma per indicare uno stato.

Decorazione

Tono, personalità, ritmo visivo. Va usata con misura: troppa decorazione distrae dal contenuto.

Regola pratica

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.

Il sito deve funzionare anche senza movimento

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".

Accessibilità

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.

Versione stabile

Profilo aggiornato

Il messaggio è leggibile senza attendere un'animazione.

Con enhancement

Profilo aggiornato

Stesso contenuto, con una breve entrata dal basso.

Transition: interpolazione automatica

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);
}

Le componenti di transition

La 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:

  1. transition-property: quale proprietà CSS deve cambiare nel tempo. Può essere una singola proprietà, una lista separata da virgole o all.
  2. transition-duration: quanto dura il passaggio. Si scrive in secondi (0.3s) o millisecondi (300ms).
  3. transition-timing-function: con che ritmo avanza la transizione. È la parte che cambia il feeling del movimento.
  4. 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;
}

Regola pratica

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.

property transform
duration 300ms
timing ease-in-out
delay 100ms

Card hover

Passate sopra la card: aspetta 100 ms, poi si solleva.

Proprietà e durata

Per definire una transition servono almeno due informazioni:

  1. quale proprietà animare;
  2. quanto deve durare il passaggio.
.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;
}

Regola pratica

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.

Card interattiva

Lo stesso markup cambia comportamento a seconda delle proprietà animate.

.card {
  transition: transform 250ms;
}

.card:hover {
  transform: translateY(-6px);
}

Evitare transition: all

Il 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.

Errore comune

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.

Con all

Card profilo

Anche il colore del testo viene animato.

Proprietà esplicite

Card profilo

Si muovono solo transform e ombra.

Timing function: il ritmo

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

Nota didattica

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.

linear
ease
ease-in
ease-out
ease-in-out

Durate realistiche

Non esiste una durata "perfetta", ma molti micro-movimenti di interfaccia funzionano bene tra 200ms e 500ms.

  • hover di un bottone: circa 150-250 ms;
  • card che entra in scena: circa 250-400 ms;
  • modale che appare: circa 350-500 ms;
  • uscita di un elemento che l'utente ha chiuso: spesso più rapida dell'entrata.
.button {
  transition: transform 180ms ease-out;
}

.modal {
  transition:
    opacity 300ms ease-out,
    transform 400ms ease-out;
}

Regola pratica

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

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.

Regola pratica

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;
}

Entrata e uscita: ritmi diversi

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;
}

Doom flicker

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.

Regola pratica

Quando un'animazione sposta un elemento e quell'elemento è anche il trigger dell'hover, applicate il movimento a un figlio.

Fragile

Il trigger si muove sotto il cursore.

Corretto

Il trigger resta fermo, si muove la faccia visiva.

Cosa possiamo animare?

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.

Interpolabili

  • Numeri con unità: width, height, font-size, border-radius, opacity, left, top
  • Colori: color, background-color, border-color
  • Trasformazioni: translate, scale, rotate, dentro transform
  • Ombre: box-shadow, text-shadow

Discrete

  • display: da block a flex cambia a scatto
  • font-family: non esiste un "mezzo font" tra Arial e Georgia
  • position: cambia il modello di posizionamento
  • height: 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;
}

Nota compatibilità

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.

Interpolabile

Opacity, transform e background-color cambiano gradualmente.

Discreta

Il layout cambia a scatto: non c'è uno stato intermedio utile.

block flex

Opacity + visibility

Il fade è fluido; l'interazione si disattiva alla fine.

Elemento cliccabile
Provate voi!

Card che si alzano senza flicker

Aprite 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.

HTML

<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>

CSS di partenza

/* 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;
}

Cosa esplorare

  • Aggiungete una transition su .card che animi transform e box-shadow.
  • In .card-link:hover .card, usate transform: translateY(-8px) e una box-shadow più marcata.
  • Provate, come errore controllato, a mettere il transform direttamente su .card-link: cosa cambia rispetto al doom flicker?
  • Replicate lo stesso effetto su .card-link:focus .card: la card deve sollevarsi anche con il tasto Tab.
  • Sfida: aggiungete un secondo livello di profondità con box-shadow a due strati.

Keyframe animations: sequenze indipendenti dallo stato

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;
}

Idea guida

@keyframes definisce un movimento riusabile. animation lo applica. I due pezzi possono vivere in punti diversi del foglio di stile.

Pannello entrato

La sequenza parte quando la classe è applicata.

@keyframes + animation

Un'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.

Errore comune

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.

Nuovo
@keyframes pop-in {
  from {
    opacity: 0;
    transform: scale(0.8);
  }

  to {
    opacity: 1;
    transform: scale(1);
  }
}

.badge {
  animation: pop-in 300ms ease-out;
}

Movimento + opacità

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;
}

Regola pratica

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.

Nota

Opacità e trasformazione lavorano nello stesso keyframe.

Ripetere un'animazione

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;
}

Errore comune

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.

fermo
3 volte
infinite

Percentuali nei keyframe

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.

Idea guida

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.

0% 60% 100%
Badge

Direzione dei keyframe

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.

normal
reverse
alternate
alternate-reverse

Shorthand animation

Quasi 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;
}

Regola pratica

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;
}
Aggiornamento salvato
.toast {
  animation: slide-in 400ms ease-out both;
  animation-delay: 300ms;
}

Lo stato finale non viene mantenuto

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.

Idea guida

Dopo l'animazione, l'elemento torna a essere quello descritto dal CSS normale. Per fissare lo stato finale serve animation-fill-mode.

box

Fill mode: prima e dopo l'animazione

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.

Regola pratica

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.

Messaggio salvato

Provate voi!

Un pulsante di aiuto che entra in scena

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.

Risultato atteso

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;
}

Cosa esplorare

  • Posizionate il pulsante in basso a destra con position: fixed, right e bottom.
  • Create un @keyframes slide-in che parta da translateY(120%) e opacity: 0 fino allo stato finale.
  • Applicate l'animazione al pulsante con durata di circa 500 ms ed easing ease-out.
  • Aggiungete animation-delay: 500ms e provate prima senza fill mode.
  • Provate poi animation-fill-mode: backwards e infine both: quale risultato è più pulito?
  • Sfida: create una seconda versione con un'icona SVG o un testo diverso, sempre con la stessa animazione.

Scegliere lo strumento

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

Regola pratica

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.

transition
animation Notifica
animation + delay Modale
animation infinite

Mettere in pausa

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");
});

Idea guida

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.

Stati CSS e azioni percepite

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.

Dettagli ordine

Il pannello entra con calma e si chiude più in fretta.

Orchestrazione: delay scalati

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;
}

Idea guida

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.

Cosa fa il browser in ogni frame

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.

  1. Style: il browser decide quali regole CSS valgono per ogni elemento.
  2. Layout: calcola posizione e dimensione di ogni elemento.
  3. Paint: colora i pixel di ogni livello.
  4. Compositing: assembla i livelli sullo schermo.

Regola pratica

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

Muovere senza ricalcolare tutto

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;
}

Errore comune

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-top

A vicino

transform

B vicino

prefers-reduced-motion

La 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.

Accessibilità

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.

Card

Passate sopra la card: il cambio di stato resta disponibile anche quando la transizione viene ridotta.

Provate voi!

Animazione opzionale con reduced motion

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.

Operazione completata

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.

HTML

<article class="notice">
  <h2>Operazione completata</h2>
  <p>Il vostro profilo è stato aggiornato.</p>
</article>

CSS di partenza

/* 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;
}

Cosa esplorare

  • Create un @keyframes enter che parta da opacity: 0 e transform: translateY(24px) e arrivi allo stato visibile.
  • Applicate animation: enter 400ms ease-out both solo dentro @media (prefers-reduced-motion: no-preference).
  • Fuori dalla media query la card deve essere già leggibile, senza dipendere dall'animazione.
  • Provate una variante leggera che usi solo opacity e nessun movimento.
  • Bonus: cambiate l'impostazione del sistema operativo, se possibile, e verificate il risultato.

Animazioni guidate dallo scroll

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.

Modello mentale

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.

Nota compatibilità

Queste API sono moderne. Le presentiamo come progressive enhancement: se il browser non le supporta, il contenuto deve restare visibile e usabile senza animazione.

Timeline del tempo

Parte da sola e avanza anche se non fate nulla.

Timeline dello scroll

Scorrete il riquadro: il progresso dipende dalla posizione.

Scorrete
Card guidata dallo scroll
Fine

La timeline è la viewport

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.

Regola pratica

Nello shorthand animation non si può inserire animation-timeline. Dichiaratela su una riga separata, dopo lo shorthand, altrimenti rischiate di sovrascriverla.

Scorrete dentro il riquadro
Primo contenuto
Secondo contenuto
Terzo contenuto
Fine del riquadro

animation-range: la parte di scroll che conta

animation-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

Regola pratica

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.

entry

Scorrete
Si stabilizza mentre entra.
Fine

cover

Scorrete
Continua più a lungo.
Fine
Provate voi!

Card rivelate dallo scroll

Risultato 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.

Scorrete
Primo contenuto
Secondo contenuto
Terzo contenuto
Fine

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.

HTML

<main class="timeline">
  <article class="reveal">Primo contenuto</article>
  <article class="reveal">Secondo contenuto</article>
  <article class="reveal">Terzo contenuto</article>
</main>

CSS di partenza

/* 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;
}

Cosa esplorare

  • Create un @keyframes reveal che parta da opacity: 0 e transform: translateY(40px).
  • Applicate l'animazione a .reveal con animation: reveal linear both.
  • Aggiungete animation-timeline: view() su riga separata.
  • Provate animation-range: entry e confrontatelo con cover.
  • Verifica del fallback: commentate le tre righe di animazione e controllate che le card siano comunque leggibili.
  • Bonus: aggiungete @media (prefers-reduced-motion: no-preference) attorno alle regole di animazione.

Transizioni tra viste

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.

Idea guida

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.

Nota compatibilità

È una feature moderna. Non è un requisito per far funzionare il sito: se il browser non la supporta, il link cambia pagina normalmente, senza transizione.

https://demo.test/progetti
Vecchia vista Elenco
Nuova vista Dettaglio

Due pagine, stesso origin

Perché una transizione tra due documenti funzioni, servono condizioni semplici:

  • le due pagine devono appartenere allo stesso sito, cioè allo stesso origin;
  • entrambe devono dichiarare @view-transition { navigation: auto; };
  • la navigazione deve essere normale: un click su un link, un submit, un cambio di hash.
@view-transition {
  navigation: auto;
}

Niente JavaScript obbligatorio per il caso MPA base. Il browser si occupa di tutto.

La vecchia pagina e la nuova pagina

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.

Provate la demo reale

Codice usato dalla demo esterna

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.

Regola pratica

Partite dalla transizione di default del browser, un fade incrociato. Personalizzatela solo se il movimento aiuta l'utente a orientarsi.

Esercizio

Laboratorio a step

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.

Riferimento visivo

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.

Step 1 - Card interattive

  • Create una griglia di almeno 3 card cliccabili.
  • Su :hover e su :focus, sollevate il contenuto interno con transform: translateY(...).
  • Aggiungete una transition breve su transform e box-shadow.
  • Separate trigger ed effetto per evitare il doom flicker.

Step 2 - Animazione keyframe

  • Aggiungete una notifica o un help button posizionato fixed.
  • Fatelo entrare con @keyframes, combinando opacity e transform.
  • Usate animation-fill-mode: both per evitare il bug del ritorno.

Step 3 - Reduced motion

  • Avvolgete le regole principali in @media (prefers-reduced-motion: no-preference).
  • Fuori dalla media query, lo stato finale deve restare leggibile.
  • Se possibile, attivate la preferenza nelle impostazioni di sistema e ricaricate la pagina.

Step 4 - Scroll reveal

  • Aggiungete una sezione lunga con più card sotto la hero.
  • Applicate animation-timeline: view() e animation-range: entry.
  • Lasciate visibile lo stato base delle card: il fallback non è opzionale.

Step 5 - Bonus View Transition

  • Duplicate la pagina creando dettagli.html.
  • Aggiungete un link reciproco tra le due pagine.
  • Inserite @view-transition { navigation: auto; } in entrambe.
  • Personalizzate ::view-transition-old(root) e ::view-transition-new(root) con un movimento sobrio.

Checklist finale

  • Gli stati :hover e :focus sono coperti entrambi.
  • Il sito funziona ed è leggibile anche senza animazioni.
  • Le proprietà animate sono scelte con criterio, con preferenza per transform e opacity.
  • Le API moderne sono usate come progressive enhancement, mai come requisito.
  • Gli accenti italiani sono corretti e i bottoni hanno un nome accessibile.

Grazie.

Fonti: Josh W. Comeau e MDN Web Docs su CSS Animations, Scroll-driven animations e View Transitions API.

1 / 42
Indice slide · ⌘K

Indice slide

⌘K