@starting-style: Animazioni di Entrata Native in CSS

@starting-style: Animazioni di Entrata Native in CSS

Perché @starting-style nel 2026

Per anni animare l’ingresso di un elemento nel DOM ha richiesto JavaScript. Si aspettava il DOMContentLoaded, si aggiungeva una classe con un minimo ritardo, si gestivano i casi edge delle race condition. Con React, Vue o Svelte la situazione è migliorata, ma il problema di fondo rimaneva: il CSS non aveva modo di sapere che un elemento stava “nascendo”. Si vedeva solo lo stato attuale, non la transizione dallo zero.

La regola @starting-style risolve esattamente questo problema. Introdotta nella specifica CSS Transitions Level 2 e ormai supportata in tutti i browser moderni, permette di definire lo stile iniziale di un elemento prima che venga renderizzato per la prima volta — consentendo al browser di interpolare verso lo stile finale usando le normali transition CSS.

Il risultato? Animazioni di entrata fluide, accessibili e performanti, con zero linee di JavaScript. Niente librerie, niente requestAnimationFrame, niente hack con setTimeout(fn, 0). Se hai già lavorato con le transizioni CSS fluide sai quanto sia prezioso avere strumenti nativi. @starting-style è il passo successivo.

In questa guida vedremo come funziona la regola, i casi d’uso più comuni, le limitazioni attuali e come integrarla in progetti reali — anche quando devi supportare browser più vecchi.

Come Funziona @starting-style: La Teoria

Il problema classico delle transizioni CSS è che funzionano solo quando un elemento cambia da uno stile a un altro. Se un elemento appare nel DOM già con opacity: 1, il browser non ha uno stato precedente da cui interpolare: nessuna transizione viene attivata.

@starting-style introduce un concetto nuovo: lo stile prima del primo render. Puoi specificare i valori iniziali da cui partire, e il browser userà le transition definite sull’elemento per animare verso lo stile corrente.

La sintassi base è questa:

.card {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 0.4s ease, transform 0.4s ease;
}

@starting-style {
  .card {
    opacity: 0;
    transform: translateY(24px);
  }
}

Quando .card compare nel DOM (o viene aggiunto tramite JavaScript), il browser legge i valori di @starting-style come punto di partenza, poi interpola verso opacity: 1 e transform: translateY(0) usando la transizione di 0.4 secondi. Semplice, pulito, nativo.

Sintassi Completa e Casi d’Uso Pratici

@starting-style può essere scritto in due forme: standalone oppure annidato dentro la regola dell’elemento. Entrambe sono valide, ma quella annidta è spesso più leggibile quando si lavora in component-based style sheets.

/* Forma standalone */
@starting-style {
  .notification {
    opacity: 0;
    transform: translateX(100%);
  }
}

/* Forma annidата (CSS nesting) */
.notification {
  opacity: 1;
  transform: translateX(0);
  transition: opacity 0.3s ease-out, transform 0.3s ease-out;

  @starting-style {
    opacity: 0;
    transform: translateX(100%);
  }
}

La forma annidta richiede CSS Nesting, supportato in tutti i browser moderni dal 2024. Se devi supportare ambienti legacy, usa la forma standalone.

Ecco i casi d’uso più comuni dove @starting-style brilla:

  • Toast e notifiche: appaiono scorrevoli dal bordo dello schermo senza una riga di JS
  • Modal e dialog: fade-in + scale quando display: block viene attivato
  • Card in una lista: ogni card che viene inserita nel DOM parte da opacity: 0
  • Tooltip: appaiono con una transizione fluida al primo hover
  • Elementi popover: integrazione nativa con la Popover API

Integrazione con display: none e Popover API

Uno dei casi più potenti di @starting-style è la combinazione con display: none e con la Popover API. In passato, animare un elemento che passava da display: none a display: block era impossibile con sole transizioni CSS: il cambio di display era istantaneo e non interpolabile.

Con la proprietà allow-discrete introdotta in CSS Transitions Level 2, e combinata con @starting-style, ora è possibile:

.dialog {
  display: none;
  opacity: 0;
  transform: scale(0.9);
}

.dialog[open],
.dialog:popover-open {
  display: block;
  opacity: 1;
  transform: scale(1);
  transition:
    opacity 0.25s ease,
    transform 0.25s ease,
    display 0.25s allow-discrete;
}

@starting-style {
  .dialog[open],
  .dialog:popover-open {
    opacity: 0;
    transform: scale(0.9);
  }
}

Con transition: display 0.25s allow-discrete, il browser aspetta la fine della transizione prima di rimuovere il display: block. Questo permette anche l’animazione di uscita, non solo di entrata. Se vuoi approfondire come la Popover API funziona in dettaglio, abbiamo una guida dedicata che copre tooltip, menu e dialog accessibili.

Ecco un esempio completo con la Popover API HTML:

<button popovertarget="my-popover">Apri Popover</button>

<div id="my-popover" popover>
  <p>Contenuto del popover con animazione nativa!</p>
</div>

<style>
  [popover] {
    opacity: 1;
    transform: translateY(0);
    transition:
      opacity 0.3s ease,
      transform 0.3s ease,
      overlay 0.3s allow-discrete,
      display 0.3s allow-discrete;
  }

  [popover]:not(:popover-open) {
    opacity: 0;
    transform: translateY(-8px);
  }

  @starting-style {
    [popover]:popover-open {
      opacity: 0;
      transform: translateY(-8px);
    }
  }
</style>

Zero JavaScript. Il browser gestisce apertura, chiusura, focus management e animazioni in modo completamente nativo. è esattamente il tipo di animazione CSS moderna che riduce la complessità del codice mantenendo un’esperienza utente di qualità.

Stagger e Animazioni a Cascata con @starting-style

Una delle tecniche più richieste nelle UI moderne è lo stagger: ogni elemento di una lista appare con un piccolo ritardo progressivo. Con JavaScript e GSAP era semplice ma costoso in termini di bundle size. Con @starting-style e variabili CSS, puoi ottenere lo stesso effetto nativamente:

.list-item {
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity 0.4s ease var(--delay, 0s),
    transform 0.4s ease var(--delay, 0s);
}

@starting-style {
  .list-item {
    opacity: 0;
    transform: translateY(16px);
  }
}

/* Assegna il delay via CSS o JS inline style */
.list-item:nth-child(1) { --delay: 0ms; }
.list-item:nth-child(2) { --delay: 60ms; }
.list-item:nth-child(3) { --delay: 120ms; }
.list-item:nth-child(4) { --delay: 180ms; }
.list-item:nth-child(5) { --delay: 240ms; }

Se la lista viene generata dinamicamente (es. con React o JavaScript), puoi impostare --delay via style inline:

// Generazione dinamica della lista con stagger
const items = ["Home", "Chi siamo", "Servizi", "Contatti"];
const list = document.querySelector(".nav-list");

items.forEach((item, index) => {
  const li = document.createElement("li");
  li.className = "list-item";
  li.style.setProperty("--delay", `${index * 60}ms`);
  li.textContent = item;
  list.appendChild(li);
});
// Ogni li appare con @starting-style: nessun setTimeout necessario

Questa tecnica si sposa bene con i framework moderni. In un contesto React 19 con Server Components, puoi passare l’indice come prop e impostare il CSS custom property direttamente nel markup server-side — zero idratazione JavaScript necessaria per le animazioni.

Browser Support e Fallback Strategy

A metà 2026, il supporto a @starting-style è ottimo:

  • Chrome/Edge 117+: supporto completo
  • Firefox 129+: supporto completo
  • Safari 17.5+: supporto completo
  • Browser meno recenti: l’elemento appare senza animazione (fallback naturale)

Il fallback naturale è già ottimo: se il browser non supporta @starting-style, l’elemento appare semplicemente senza la transizione di entrata. Nessun errore, nessun contenuto nascosto. è progressive enhancement by design.

Se vuoi essere esplicito nel supporto, usa @supports:

/* Animazione JS come fallback base */
.card {
  opacity: 1;
  transform: translateY(0);
}

/* Animazione nativa solo se supportata */
@supports (selector(:has(*))) {
  .card {
    transition: opacity 0.4s ease, transform 0.4s ease;
  }

  @starting-style {
    .card {
      opacity: 0;
      transform: translateY(20px);
    }
  }
}

Nota: non esiste un @supports at-rule(@starting-style) diretto ancora. Un workaround comune è usare un feature che condivide il browser support. In alternativa, la rilevazione via JavaScript è banale:

// Feature detection per @starting-style
const supportsStartingStyle = CSS.supports("selector(:has(*))") &&
  typeof CSSStartingStyleRule !== "undefined";

if (!supportsStartingStyle) {
  // Applica fallback JavaScript animation
  document.querySelectorAll(".card").forEach((card, i) => {
    card.style.opacity = "0";
    card.style.transform = "translateY(20px)";
    setTimeout(() => {
      card.style.transition = "opacity 0.4s ease, transform 0.4s ease";
      card.style.opacity = "1";
      card.style.transform = "translateY(0)";
    }, i * 60);
  });
}

Per chi lavora su progetti con requisiti SEO elevati, è importante che le animazioni non blocchino il rendering del contenuto. Su questo tema, la nostra guida SEO tecnica per developer copre tutti gli aspetti critici, inclusa l’ottimizzazione delle transizioni CSS per i Core Web Vitals.

FAQ e Domande Frequenti

@starting-style funziona solo al primo render o anche quando l’elemento torna visibile?

@starting-style si attiva ogni volta che un elemento non ha uno stile precedente calcolato. Questo include: la prima volta che viene inserito nel DOM, ma anche quando un elemento passa da display: none a display: block (perché in quel caso il browser non ha uno stile precedente da interpolare). Invece, se l’elemento è già nel DOM con visibility: hidden o opacity: 0, la regola non si attiva al passaggio a visibile — in quel caso si usa normalmente una transizione.

Posso usare @starting-style con animazioni CSS (@keyframes) invece che con transition?

No, @starting-style è specificamente legato alle CSS transitions, non alle @keyframes. Se vuoi animazioni di entrata complesse (come rimbalzo, elasticità o effetti multipli in sequenza), devi usare @keyframes normalmente con la proprietà animation. La differenza chiave è che le keyframe animations si attivano sempre quando la classe viene applicata, quindi non hanno bisogno di @starting-style — ma di conseguenza non possono fare l’animazione bidirezionale (entrata/uscita) in modo semplice come le transition.

Come si comporta @starting-style con prefers-reduced-motion?

Come qualsiasi altra transizione CSS, @starting-style va combinato con la media query prefers-reduced-motion per rispettare le preferenze di accessibilità degli utenti. La best practice è sempre la stessa:

@media (prefers-reduced-motion: no-preference) {
  .card {
    transition: opacity 0.4s ease, transform 0.4s ease;
  }

  @starting-style {
    .card {
      opacity: 0;
      transform: translateY(20px);
    }
  }
}

/* Con reduced-motion: nessuna transizione,
   l'elemento appare direttamente senza @starting-style */

Strutturando il CSS in questo modo, gli utenti che hanno preferenze di riduzione del movimento vedranno l’elemento apparire direttamente, senza nessuna transizione. È progressive enhancement e rispetto dell’accessibilità insieme.

@starting-style può sostituire completamente librerie come GSAP o Framer Motion?

Dipende dal caso d’uso. Per animazioni di entrata semplici e di media complessità (fade, slide, scale), @starting-style è una soluzione eccellente e più performante di qualsiasi libreria JavaScript. Per animazioni complesse come scrollytelling, physics-based animations, o sequenze orchestrate con timeline, GSAP rimane insostituibile. La buona notizia è che non è una scelta binaria: puoi usare @starting-style per le animazioni di entrata semplici e riservare GSAP ai casi realmente complessi, riducendo così il bundle size del 70-80% nella maggior parte dei progetti. Se stai valutando dove investire in animazioni per un micro-SaaS, questo tipo di scelta è parte del processo di sviluppo come developer solitario.

Conclusione

@starting-style è una di quelle feature CSS che sembrano piccole ma cambiano concretamente il modo di scrivere interfacce. Non è un’alternativa totale a JavaScript per le animazioni, ma è la risposta nativa e performante a un problema che per troppo tempo ha richiesto workaround fragili.

Il suo punto di forza è la semplicità concettuale: definisci lo stile di partenza, definisci lo stile finale, aggiungi una transizione. Il browser fa il resto. Nessuna dipendenza, nessun bundle size aggiuntivo, nessuna gestione manuale del ciclo di vita degli elementi.

Con il supporto nei browser moderni ormai consolidato e il fallback naturale per i browser più vecchi, non ci sono scuse per non iniziare a usarla oggi. Inizia dai casi più semplici — toast, notifiche, card — e vedrai quanto codice JavaScript puoi eliminare dal tuo progetto. Il CSS nativo è più potente che mai nel 2026.

Suggerimenti e Risorse

🔧 Strumento: Usa animation-timeline e view-timeline insieme a @starting-style per creare esperienze scroll-driven completamente native. Le due spec si completano: @starting-style gestisce l’entrata iniziale, animation-timeline gestisce la progressione durante lo scroll.

💡 Pro tip: Quando usi @starting-style con la Popover API, aggiungi sempre overlay 0.3s allow-discrete alla lista delle transition. Questo permette all’elemento di uscire dall’overlay layer solo dopo che la transizione di uscita è completata, evitando il flash visivo.

🎯 Strategia: Adotta @starting-style come primo approccio per tutte le animazioni di entrata. Usa JavaScript solo se hai bisogno di logica condizionale, sequenze complesse o supporto per browser molto vecchi. Seguendo questa strategia, la performance del sito migliora perché le animazioni CSS vengono gestite dal compositor thread, senza bloccare il main thread.

Condividi

Articoli Recenti

Categorie popolari