CSS ti offre due strumenti per il movimento: le transizioni e le animazioni @keyframes. La maggior parte degli sviluppatori sa che esistono entrambi, ma si rivolge a quello che ha imparato per primo — di solito le transizioni — e poi si chiede perché lo spinner di caricamento non si ripeta in loop o perché la notifica torna alla posizione originale. La decisione non è complicata una volta che capisci a cosa serve ciascuno. Le transizioni sono una reazione: qualcosa cambia stato, il browser ammorbidisce il cambiamento. Le animazioni @keyframes sono script autonomi: girano da soli, si ripetono se vuoi, e non necessitano di un trigger di stato. Questa guida copre entrambi, le proprietà che contano di più, e i dettagli sulle performance che separano le animazioni fluide a 60fps da quelle scattose. Se scrivi molto CSS, vale la pena aggiungere ai segnalibri il CSS Formatter — pulirà rapidamente qualsiasi shorthand disordinato.
Transizioni CSS — Il Caso Semplice
Una transizione dice al browser: "quando questa proprietà cambia, anima il cambiamento nel tempo invece di saltare." Tutto qui. L'uso più comune è per gli effetti hover — cambiamenti di colore, dissolvenze di opacità, sottili trasformazioni di scala. La guida MDN sulle transizioni copre la specifica completa, ma le quattro proprietà di cui ti importa sono transition-property, transition-duration, transition-timing-function e transition-delay.
/* The shorthand: property duration easing delay */
.btn-primary {
background-color: #4f46e5;
color: #fff;
padding: 0.625rem 1.25rem;
border-radius: 6px;
border: none;
cursor: pointer;
transition: background-color 200ms ease, transform 150ms ease;
}
.btn-primary:hover {
background-color: #4338ca;
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(0);
}
/* Opacity fade — a card coming into view */
.notification-card {
opacity: 0;
transition: opacity 300ms ease-in-out;
}
.notification-card.is-visible {
opacity: 1;
}Puoi elencare più transizioni separate da virgole, ognuna con la sua durata ed easing — è quello che fa l'esempio del pulsante sopra. Ogni proprietà si anima in modo indipendente.
transition: all. È allettante come soluzione universale, ma significa che ogni cambio di proprietà su quell'elemento viene animato — inclusi quelli che non intendevi. Fa anche lavorare di più il browser su ogni frame. Elenca le proprietà specifiche che vuoi transitare. I docs MDN su transition-property spiegano quali proprietà sono animabili.transition-timing-function — L'Easing Spiegato
La funzione di temporizzazione controlla la curva di accelerazione dell'animazione — se inizia veloce e rallenta, si muove a ritmo costante, o rimbalza attraverso una curva personalizzata. Le parole chiave predefinite coprono la maggior parte delle situazioni.
ease— inizia lento, accelera, poi rallenta verso la fine. Il default. Buono per la maggior parte dei movimenti UI.linear— velocità costante per tutta la durata. Buono per spinner e barre di progresso, pessimo per la maggior parte delle altre cose — sembra meccanico.ease-in— inizia lento, finisce veloce. Sembra qualcosa che prende velocità. Buono per gli elementi che escono dallo schermo.ease-out— inizia veloce, finisce lento. Sembra naturale per gli elementi che entrano nello schermo (come una notifica che scivola dentro).ease-in-out— lento su entrambe le estremità. Appare rifinito per gli elementi che iniziano e si fermano in vista.cubic-bezier(x1, y1, x2, y2)— definisci la tua curva. Strumenti come cubic-bezier.com ti permettono di costruire e vedere in anteprima curve personalizzate visivamente.
/* Custom spring-like easing with cubic-bezier */
.drawer {
transform: translateX(-100%);
transition: transform 350ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
.drawer.is-open {
transform: translateX(0);
}
/* The cubic-bezier values above overshoot slightly (y > 1)
which gives a satisfying spring effect on open */@keyframes — Quando le Transizioni Non Bastano
Le transizioni hanno un limite preciso: hanno bisogno di due stati e un trigger. Se vuoi che qualcosa si ripeta all'infinito, parta al caricamento della pagina senza alcuna interazione, o si animi attraverso più di due passi, hai bisogno di @keyframes. Il riferimento MDN per @keyframes copre la sintassi completa. Definisci i keyframe separatamente, poi li colleghi a un elemento con la proprietà animation.
/* Define the animation */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
/* Attach it to an element */
.spinner {
width: 24px;
height: 24px;
border: 3px solid #e5e7eb;
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 700ms linear infinite;
}
/* Multi-step — more than two stops */
@keyframes pulse-ring {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.15); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
.badge-live {
animation: pulse-ring 1.5s ease-in-out infinite;
}Lo Shorthand animation — Tutte e Otto le Proprietà
Lo shorthand animation comprime otto proprietà in una sola dichiarazione. Non ne hai bisogno tutte ogni volta, ma sapere cosa controlla ognuna ti evita di scavare nella documentazione quando qualcosa non si comporta come previsto.
/*
animation: name | duration | timing-function | delay | iteration-count | direction | fill-mode | play-state
*/
.toast {
animation: slide-in-right 350ms ease-out 0s 1 normal forwards running;
}
/* More commonly written as just the values you need: */
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
/* Breaking it out to individual properties for clarity: */
.confetti-piece {
animation-name: float-down;
animation-duration: 2s;
animation-timing-function: ease-in;
animation-delay: calc(var(--i) * 150ms); /* staggered via CSS custom property */
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: forwards;
animation-play-state: running;
}
/* Pausing an animation via JavaScript: toggle a class */
.spinner.is-paused {
animation-play-state: paused;
}animation-fill-mode — Risolvere il Problema dello Snap-Back
Questa è la proprietà che mette tutti in difficoltà prima o poi. Per default, quando un'animazione finisce, l'elemento torna bruscamente ai suoi stili pre-animazione — come se l'animazione non fosse mai avvenuta. Se animi una notifica toast che scivola da destra, questa tornerà di scatto fuori dallo schermo nel momento in cui l'animazione termina. animation-fill-mode: forwards risolve questo mantenendo l'elemento nel suo stato finale dell'ultimo keyframe una volta terminata l'animazione.
@keyframes slide-in-right {
from {
transform: translateX(110%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Without forwards: toast slides in, then SNAPS back off-screen */
.toast-bad {
animation: slide-in-right 350ms ease-out;
}
/* With forwards: toast slides in and STAYS in position */
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
/*
fill-mode values:
- none (default) — no styles applied before or after
- forwards — hold the final keyframe after the animation ends
- backwards — apply the first keyframe during the delay period
- both — forwards + backwards combined
*/Esempi Reali — Skeleton Loader, Spinner, Toast, Badge Pulsante
Ecco quattro pattern che userai costantemente. Ognuno dimostra una combinazione specifica di @keyframes e proprietà di animazione.
/* --- 1. Skeleton loading shimmer --- */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(
90deg,
#f0f0f0 25%,
#e0e0e0 50%,
#f0f0f0 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite linear;
border-radius: 4px;
}
.skeleton-title { height: 20px; width: 60%; margin-bottom: 12px; }
.skeleton-text { height: 14px; width: 100%; margin-bottom: 8px; }
.skeleton-text:last-child { width: 80%; }/* --- 2. Spinning loader --- */
@keyframes spin {
to { transform: rotate(360deg); }
}
.loader-spinner {
display: inline-block;
width: 32px;
height: 32px;
border: 3px solid rgba(79, 70, 229, 0.2);
border-top-color: #4f46e5;
border-radius: 50%;
animation: spin 600ms linear infinite;
}/* --- 3. Slide-in notification toast --- */
@keyframes toast-enter {
from {
transform: translateX(calc(100% + 1.5rem));
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes toast-exit {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(calc(100% + 1.5rem));
opacity: 0;
}
}
.toast {
position: fixed;
bottom: 1.5rem;
right: 1.5rem;
padding: 0.875rem 1.25rem;
background: #1f2937;
color: #fff;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
animation: toast-enter 350ms ease-out forwards;
}
.toast.is-dismissing {
animation: toast-exit 300ms ease-in forwards;
}/* --- 4. Pulsing notification badge --- */
@keyframes badge-pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.5);
}
50% {
box-shadow: 0 0 0 6px rgba(239, 68, 68, 0);
}
}
.notification-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 20px;
height: 20px;
padding: 0 5px;
background: #ef4444;
color: #fff;
font-size: 11px;
font-weight: 700;
border-radius: 999px;
animation: badge-pulse 2s ease-in-out infinite;
}Performance — Proprietà Compositate dalla GPU vs Proprietà che Attivano il Layout
Non tutte le proprietà CSS sono uguali quando si tratta di performance delle animazioni. La pipeline di rendering del browser ha tre stadi che contano: layout (calcolo delle dimensioni e posizioni degli elementi), paint (riempimento dei pixel) e composite (combinazione dei layer sulla GPU). Animare una proprietà che attiva il layout significa che il browser ricalcola l'intera geometria del documento ad ogni frame — è costoso, ed è quello che causa i scatti. La guida alle performance delle animazioni di web.dev copre questo argomento in profondità.
- Sicuro da animare (compositate dalla GPU):
transform(translate, scale, rotate) eopacity. Girano interamente sul thread del compositing della GPU — non attivano mai il layout o il paint. - Causa paint (evitare se possibile):
color,background-color,border-color,box-shadow. Queste saltano il layout ma attivano un repaint ad ogni frame. Le transizioni brevi (sotto i 300ms) di solito vanno bene. - Causa layout (non animare mai):
width,height,top,left,margin,padding. Ogni frame attiva un ricalcolo completo del layout — scatti garantiti su pagine complesse.
transform e opacity. Se vuoi spostare qualcosa, usa translate non left/top. Se vuoi ridimensionare qualcosa, usa scale non width/height. CSS Triggers è un riferimento che elenca esattamente quali stadi di rendering colpisce ogni proprietà./* Bad — animating width triggers layout on every frame */
.progress-bar-bad {
transition: width 300ms ease;
}
/* Good — use scaleX transform instead */
.progress-bar {
transform-origin: left center;
transition: transform 300ms ease;
}
/* In JS, set: progressBar.style.transform = 'scaleX(0.75)' for 75% */
/* Bad — animating top/left (positions element with layout) */
.tooltip-bad {
position: absolute;
top: 0;
transition: top 200ms ease;
}
/* Good — use translateY instead */
.tooltip {
position: absolute;
top: 0;
transform: translateY(0);
transition: transform 200ms ease;
}will-change — Usalo con Parsimonia
will-change è un suggerimento al browser che una proprietà specifica sta per essere animata, così dovrebbe promuovere l'elemento al suo layer compositor in anticipo. Questo può eliminare il breve scatto che a volte vedi all'inizio di un'animazione su hardware di fascia bassa. Ma ha un costo reale: ogni layer promosso consuma memoria GPU. Se metti will-change: transform su ogni elemento animato nella tua app, degraderai le performance sulla maggior parte dei dispositivi — l'opposto di quello che volevi.
/* Right way — add it just before animation starts, remove after */
.modal-overlay {
/* Don't set will-change here by default */
}
/* Add via JavaScript only when user triggers modal open */
/* overlay.style.willChange = 'opacity'; */
/* overlay.addEventListener('transitionend', () => { */
/* overlay.style.willChange = 'auto'; */
/* }); */
/* Acceptable in CSS for elements that animate frequently
(e.g., a persistent floating action button on scroll) */
.fab {
will-change: transform; /* OK — this element genuinely animates on scroll */
transition: transform 200ms ease;
}
/* Don't do this — wastes GPU memory with zero benefit */
.card {
will-change: transform; /* BAD — card only animates on hover, not constantly */
}prefers-reduced-motion — L'Accessibilità che Non Puoi Saltare
Una parte significativa degli utenti ha disturbi vestibolari o altre condizioni dove il movimento provoca disagio o nausea. La media query prefers-reduced-motion ti permette di rispettare l'impostazione "riduci movimento" a livello di sistema. La linea guida WCAG 2.1 2.3.3 copre questo requisito, e il riferimento MDN per prefers-reduced-motion mostra il supporto nei browser (è universale ora). Il pattern è semplice: avvolgi le tue animazioni nella media query standard, e fornisci un fallback senza movimento all'interno di quella a movimento ridotto.
/* Define animations normally for users who are fine with motion */
@keyframes slide-in-right {
from { transform: translateX(110%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
.toast {
animation: slide-in-right 350ms ease-out forwards;
}
.spinner {
animation: spin 600ms linear infinite;
}
/* Override for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
.toast {
/* No sliding — just appear */
animation: none;
opacity: 1;
transform: none;
}
.spinner {
/* Slow it way down or stop it entirely */
animation-duration: 4s;
}
/* Nuclear option — disable ALL animations site-wide */
/* Use this only if you haven't audited each animation individually */
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}L'"opzione nucleare" alla fine di quel snippet è un pattern comune per codebase esistenti che non hanno controllato ogni animazione. È meglio di niente, ma controllare ogni animazione individualmente ti dà più controllo — alcune animazioni trasmettono stato (una barra di avanzamento, uno spinner di caricamento) e dovrebbero essere mantenute, solo rallentate.
Transizioni vs @keyframes — La Guida alla Decisione
Se non sei sicuro di quale scegliere, fatti due domande sull'animazione che stai costruendo:
- È innescata da un cambio di stato? (hover, focus, toggle di classe, checkbox) → Usa una transizione. È esattamente quello per cui le transizioni sono progettate.
- Si ripete all'infinito? → Usa @keyframes con
animation-iteration-count: infinite. - Ha più di due passi? (non solo inizio → fine, ma inizio → mezzo → fine o di più) → Usa @keyframes.
- Deve partire al caricamento della pagina, senza alcuna interazione dell'utente? → Usa @keyframes.
- L'elemento deve rimanere nel suo stato finale dopo aver terminato? → Usa @keyframes con
animation-fill-mode: forwards.
Conclusioni
Le transizioni gestiscono il movimento guidato dallo stato. @keyframes gestisce tutto il resto. Anima sempre transform e opacity per le performance — lascia fuori width, height, top e left. Usa animation-fill-mode: forwards ogni volta che hai bisogno che l'elemento mantenga il suo stato finale. Aggiungi l'override prefers-reduced-motion prima di rilasciare qualcosa — è un'aggiunta di dieci righe che conta molto per gli utenti reali. Per ulteriori informazioni sul modello di compositing e su cosa attiva il layout, i docs sulle performance di rendering di web.dev sono la risorsa più pratica disponibile. Una volta che hai scritto il CSS, passalo attraverso il CSS Formatter per mantenere lo shorthand leggibile, o attraverso il CSS Minifier per rimuovere gli spazi bianchi prima di andare in produzione.