Transizioni per la visualizzazione dello stesso documento per le applicazioni a pagina singola

Pubblicato: 17 agosto 2021, Ultimo aggiornamento: 25 settembre 2024

Quando una transizione di visualizzazione viene eseguita in un singolo documento, si parla di transizione di visualizzazione all'interno dello stesso documento. Questo si verifica tipicamente nelle applicazioni a pagina singola (APS), in cui viene utilizzato JavaScript per aggiornare il DOM. Le transizioni di visualizzazione dello stesso documento sono supportate in Chrome a partire dalla versione 111.

Per attivare una transizione di visualizzazione nello stesso documento, chiama document.startViewTransition:

function handleClick(e) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow();
    return;
  }

  // With a View Transition:
  document.startViewTransition(() => updateTheDOMSomehow());
}

Quando viene invocato, il browser acquisisce automaticamente gli istantanei di tutti gli elementi in cui è dichiarata una proprietà CSS view-transition-name.

Poi esegue il callback passato che aggiorna il DOM, dopodiché acquisisce gli snapshot del nuovo stato.

Questi snapshot vengono poi disposti in una struttura ad albero di pseudo-elementi e animati utilizzando le potenti animazioni CSS. Le coppie di istantanee dello stato precedente e di quello nuovo passano senza interruzioni dalla posizione e dalle dimensioni precedenti alla nuova posizione, mentre i contenuti vengono visualizzati con dissolvenza incrociata. Se vuoi, puoi utilizzare il CSS per personalizzare le animazioni.


La transizione predefinita: dissolvenza incrociata

La transizione di visualizzazione predefinita è una dissolvenza incrociata, quindi è un'ottima introduzione all'API:

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

Dove updateTheDOMSomehow modifica il DOM in base al nuovo stato. Puoi farlo come preferisci. Ad esempio, puoi aggiungere o rimuovere elementi, modificare i nomi delle classi o gli stili.

Le pagine vengono visualizzate con transizione graduale:

La dissolvenza incrociata predefinita. Demo minima. Fonte.

Ok, una transizione non è poi così impressionante. Fortunatamente, le transizioni possono essere personalizzate, ma prima devi capire come funziona questa transizione in dissolvenza croce di base.


Come funzionano queste transizioni

Aggiorna l'esempio di codice precedente.

document.startViewTransition(() => updateTheDOMSomehow(data));

Quando viene chiamato .startViewTransition(), l'API acquisisce lo stato corrente della pagina. Sono incluse le istantanee.

Al termine, viene chiamato il callback passato a .startViewTransition(). È qui che viene modificato il DOM. L'API acquisisce quindi il nuovo stato della pagina.

Una volta acquisito il nuovo stato, l'API crea una struttura ad albero di pseudo-elementi come questa:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

Il ::view-transition si trova in un overlay, sopra a tutti gli altri elementi della pagina. Questa opzione è utile se vuoi impostare un colore di sfondo per la transizione.

::view-transition-old(root) è uno screenshot della vecchia visualizzazione, mentre ::view-transition-new(root) è una rappresentazione in tempo reale della nuova visualizzazione. Entrambi vengono visualizzati come "contenuti sostituiti" CSS (come <img>).

La vecchia visualizzazione viene visualizzata da opacity: 1 a opacity: 0, mentre la nuova da opacity: 0 a opacity: 1, creando una transizione sfumata.

Tutta l'animazione viene eseguita utilizzando animazioni CSS, quindi possono essere personalizzate con CSS.

Personalizzare la transizione

Tutti gli pseudo-elementi di transizione della visualizzazione possono essere scelti come target con CSS e, poiché le animazioni sono definite utilizzando CSS, puoi modificarle utilizzando le proprietà di animazione CSS esistenti. Ad esempio:

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

Con questa modifica, l'effetto dissolvenza è ora molto lento:

Transizione graduale lunga. Demo minima. Fonte.

Ok, non è ancora impressionante. Il codice seguente implementa invece la transizione dell'asse condiviso di Material Design:

@keyframes fade-in {
  from { opacity: 0; }
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes slide-from-right {
  from { transform: translateX(30px); }
}

@keyframes slide-to-left {
  to { transform: translateX(-30px); }
}

::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

Ed ecco il risultato:

Transizione dell'asse condiviso. Demo minima. Origine.

Applicare transizioni a più elementi

Nella demo precedente, l'intera pagina è coinvolta nella transizione dell'asse condiviso. Funziona per la maggior parte della pagina, ma non sembra adatto all'intestazione, in quanto esce solo per rientrare di nuovo.

Per evitare che ciò accada, puoi estrarre l'intestazione dal resto della pagina in modo da animarla separatamente. Questo viene fatto assegnando un view-transition-name all'elemento.

.main-header {
  view-transition-name: main-header;
}

Il valore di view-transition-name può essere qualsiasi (tranne none, che indica che non esiste un nome di transizione). Viene utilizzato per identificare in modo univoco l'elemento durante la transizione.

Il risultato è:

Transizione dell'asse condiviso con intestazione fissa. Demo minima. Origine.

Ora l'intestazione rimane in posizione e si attenua.

La dichiarazione CSS ha causato la modifica dell'albero degli pseudo-elementi:

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
   └─ ::view-transition-image-pair(main-header)
      ├─ ::view-transition-old(main-header)
      └─ ::view-transition-new(main-header)

Ora ci sono due gruppi di transizione. Uno per l'intestazione e un altro per il resto. Questi elementi possono essere scelti come target in modo indipendente con CSS e possono essere assegnate transizioni diverse. Tuttavia, in questo caso a main-header è stata lasciata la transizione predefinita, ovvero una dissolvenza incrociata.

Ok. La transizione predefinita non è solo una dissolvenza incrociata, ma anche la transizione ::view-transition-group:

  • Posizione e trasformazione (utilizzando un transform)
  • Larghezza
  • Altezza

Questo non ha avuto importanza fino a ora, poiché l'intestazione ha le stesse dimensioni e la stessa posizione su entrambi i lati della modifica del DOM. Ma puoi anche estrarre il testo nell'intestazione:

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

Viene utilizzato fit-content in modo che l'elemento abbia le dimensioni del testo, invece di estenderlo alla larghezza rimanente. In caso contrario, la freccia Indietro riduce le dimensioni dell'elemento di testo dell'intestazione, anziché mantenere le stesse dimensioni in entrambe le pagine.

Ora abbiamo tre parti da utilizzare:

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

Ma, di nuovo, utilizziamo i valori predefiniti:

Testo dell'intestazione scorrevole. Demo minima. Origine.

Ora il testo dell'intestazione scorre leggermente per fare spazio al pulsante Indietro.


Animare più pseudo-elementi nello stesso modo con view-transition-class

Supporto dei browser

  • Chrome: 125.
  • Bordo: 125.
  • Firefox: non supportato.
  • Safari Technology Preview: supportato.

Supponiamo che tu abbia una transizione di visualizzazione con un gruppo di schede, ma anche un titolo sulla pagina. Per animare tutte le schede tranne il titolo, devi scrivere un selettore che abbia come target ogni singola scheda.

h1 {
    view-transition-name: title;
}
::view-transition-group(title) {
    animation-timing-function: ease-in-out;
}

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }

#card20 { view-transition-name: card20; }

::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),

::view-transition-group(card20) {
    animation-timing-function: var(--bounce);
}

Hai 20 elementi? Sono 20 i selettori che devi scrivere. Vuoi aggiungere un nuovo elemento? Poi devi anche ingrandire il selettore che applica gli stili di animazione. Non è esattamente scalabile.

view-transition-class può essere utilizzato negli pseudo-elementi di transizione della visualizzazione per applicare la stessa regola di stile.

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }

#card20 { view-transition-name: card20; }

#cards-wrapper > div {
  view-transition-class: card;
}
html::view-transition-group(.card) {
  animation-timing-function: var(--bounce);
}

L'esempio di schede che segue utilizza il precedente snippet CSS. A tutte le schede, incluse quelle appena aggiunte, viene applicato lo stesso timing con un solo selettore: html::view-transition-group(.card).

Registrazione della demo di Schede. Se utilizzi view-transition-class, viene applicato lo stesso animation-timing-function a tutte le schede, tranne a quelle aggiunte o rimosse.

Eseguire il debug delle transizioni

Poiché le transizioni di visualizzazione si basano sulle animazioni CSS, il riquadro Animazioni in Chrome DevTools è ottimo per il debug delle transizioni.

Utilizzando il riquadro Animazioni, puoi mettere in pausa l'animazione successiva, quindi scorrere avanti e indietro nell'animazione. Durante questa operazione, gli pseudo-elementi di transizione sono disponibili nel riquadro Elementi.

Eseguire il debug delle transizioni di visualizzazione con Chrome DevTools
.

Gli elementi di transizione non devono necessariamente essere lo stesso elemento DOM

Finora abbiamo utilizzato view-transition-name per creare elementi di transizione separati per l'intestazione e il testo dell'intestazione. Si tratta concettualmente dello stesso elemento prima e dopo la modifica del DOM, ma puoi creare transizioni in caso contrario.

Ad esempio, all'embed del video principale può essere assegnato un view-transition-name:

.full-embed {
  view-transition-name: full-embed;
}

Poi, quando viene fatto clic sulla miniatura, è possibile assegnare lo stesso view-transition-name, solo per la durata della transizione:

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

E il risultato:

Un elemento in transizione a un altro. Demo minima. Origine.

Ora la miniatura passa all'immagine principale. Anche se sono elementi concettualmente (e letteralmente) diversi, l'API di transizione li tratta come se fossero la stessa cosa perché condividono lo stesso view-transition-name.

Il codice reale per questa transizione è un po' più complicato rispetto all'esempio precedente, in quanto gestisce anche il ritorno alla pagina delle miniature. Consulta il codice sorgente per l'implementazione completa.


Transizioni di entrata e uscita personalizzate

Osserva questo esempio:

Accesso e uscita dalla barra laterale. Demo minima. Origine.

La barra laterale fa parte della transizione:

.sidebar {
  view-transition-name: sidebar;
}

Tuttavia, a differenza dell'intestazione nell'esempio precedente, la barra laterale non viene visualizzata su tutte le pagine. Se entrambi gli stati hanno la barra laterale, gli pseudo-elementi di transizione hanno il seguente aspetto:

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

Tuttavia, se la barra laterale si trova solo nella nuova pagina, lo pseudo-elemento ::view-transition-old(sidebar) non sarà presente. Poiché non esiste un'immagine "vecchia" per la barra laterale, la coppia di immagini avrà solo un ::view-transition-new(sidebar). Allo stesso modo, se la barra laterale si trova solo nella vecchia pagina, la coppia di immagini avrà solo un ::view-transition-old(sidebar).

Nella demo precedente, la barra laterale effettua transizioni diverse a seconda che sia in entrata, in uscita o presente in entrambi gli stati. Entra scorrendo da destra e attenuandosi, esce scorrendo verso destra e attenuandosi e rimane in posizione quando è presente in entrambi gli stati.

Per creare transizioni di entrata e di uscita specifiche, puoi utilizzare la pseudoclasse :only-child per scegliere come target gli pseudoelementi vecchi o nuovi quando sono gli unici elementi secondari nella coppia di immagini:

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

In questo caso, non esiste una transizione specifica per quando la barra laterale è presente in entrambi gli stati, poiché l'impostazione predefinita è perfetta.

Aggiornamenti del DOM asincroni e attesa dei contenuti

Il callback passato a .startViewTransition() può restituire una promessa, che consente aggiornamenti DOM asincroni e l'attesa del completamento di contenuti importanti.

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

La transizione non verrà avviata finché la promessa non verrà soddisfatta. Durante questo periodo, la pagina viene bloccata, quindi i ritardi dovrebbero essere ridotti al minimo. In particolare, i recuperi di rete devono essere eseguiti prima di chiamare .startViewTransition(), mentre la pagina è ancora completamente interattiva, anziché all'interno del callback di .startViewTransition().

Se decidi di attendere che le immagini o i caratteri siano pronti, assicurati di utilizzare un timeout aggressivo:

const wait = ms => new Promise(r => setTimeout(r, ms));

document.startViewTransition(async () => {
  updateTheDOMSomehow();

  // Pause for up to 100ms for fonts to be ready:
  await Promise.race([document.fonts.ready, wait(100)]);
});

Tuttavia, in alcuni casi è meglio evitare del tutto il ritardo e utilizzare i contenuti di cui già disponi.


Sfrutta al meglio i contenuti che hai già

Se la miniatura passa a un'immagine più grande:

La miniatura passa a un'immagine più grande. Prova il sito dimostrativo.

La transizione predefinita è la dissolvenza incrociata, il che significa che la miniatura potrebbe avere una dissolvenza incrociata con un'immagine completa non ancora caricata.

Un modo per gestire questo problema è attendere il caricamento dell'immagine completa prima di avviare la transizione. Idealmente, questa operazione dovrebbe essere eseguita prima di chiamare .startViewTransition(), in modo che la pagina rimanga interattiva e sia possibile mostrare un'animazione di attesa per indicare all'utente che le risorse sono in fase di caricamento. In questo caso, però, esiste un modo migliore:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
}

Ora la miniatura non scompare, ma rimane sotto l'immagine completa. Ciò significa che se la nuova visualizzazione non è stata caricata, la miniatura sarà visibile per tutta la durata della transizione. Ciò significa che la transizione può iniziare immediatamente e l'immagine completa può essere caricata in base ai tempi necessari.

Questa soluzione non funzionerebbe se la nuova vista mostrasse trasparenza, ma in questo caso sappiamo che non lo è, quindi possiamo apportare questa ottimizzazione.

Gestire le modifiche delle proporzioni

Fortunatamente, finora tutte le transizioni sono state a elementi con le stesse proporzioni, ma non sarà sempre così. Cosa succede se la miniatura è 1:1 e l'immagine principale è in 16:9?

È in corso la transizione di un elemento a un altro, con una modifica delle proporzioni. Demo minima. Origine.

Nella transizione predefinita, il gruppo viene animato dalle dimensioni precedenti a quelle successive. Le visualizzazioni precedenti e nuove hanno una larghezza del 100% del gruppo e un'altezza automatica, il che significa che mantengono le proporzioni indipendentemente dalle dimensioni del gruppo.

Si tratta di un valore predefinito valido, ma non è quello che ci serve in questo caso. Pertanto:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
  /* Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. */
  height: 100%;
  /* Clip any overflow of the view */
  overflow: clip;
}

/* The old view is the thumbnail */
::view-transition-old(full-embed) {
  /* Maintain the aspect ratio of the view,
  by shrinking it to fit within the bounds of the element */
  object-fit: contain;
}

/* The new view is the full image */
::view-transition-new(full-embed) {
  /* Maintain the aspect ratio of the view,
  by growing it to cover the bounds of the element */
  object-fit: cover;
}

Ciò significa che la miniatura rimane al centro dell'elemento quando la larghezza si espande, mentre l'immagine completa viene "ritagliata" mentre passa da 1:1 a 16:9.

Per informazioni più dettagliate, consulta Transizioni di visualizzazione: gestione delle modifiche del formato


Utilizzare le query sui media per modificare le transizioni per diversi stati del dispositivo

Potresti voler utilizzare transizioni diverse sui dispositivi mobili rispetto ai computer, ad esempio in questo esempio viene eseguita una transizione completa dal lato su dispositivo mobile, ma una transizione più sottile su computer:

Un elemento in transizione a un altro. Demo minima. Origine.

È possibile ottenere questo risultato utilizzando normali query supporti:

/* Transitions for mobile */
::view-transition-old(root) {
  animation: 300ms ease-out both full-slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both full-slide-from-right;
}

@media (min-width: 500px) {
  /* Overrides for larger displays.
  This is the shared axis transition from earlier in the article. */
  ::view-transition-old(root) {
    animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
  }

  ::view-transition-new(root) {
    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
  }
}

Ti consigliamo inoltre di modificare gli elementi a cui assegni un view-transition-name in base alle query sui media corrispondenti.


Rispondere alla preferenza "movimento ridotto"

Gli utenti possono indicare la preferenza per la riduzione dei movimenti tramite il sistema operativo e questa preferenza è messa in evidenza in CSS.

Puoi scegliere di impedire qualsiasi transizione per questi utenti:

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Tuttavia, una preferenza per "movimento ridotto" non significa che l'utente non voglia nessun movimento. Anziché lo snippet precedente, puoi scegliere un'animazione più sottile, ma che esprima comunque la relazione tra gli elementi e il flusso di dati.


Gestire più stili di transizione delle visualizzazioni con i tipi di transizione delle visualizzazioni

Supporto dei browser

  • Chrome: 125.
  • Bordo: 125.
  • Firefox: non supportato.
  • Safari: 18.

A volte, una transizione da una determinata vista all'altra dovrebbe avere una transizione personalizzata. Ad esempio, quando passi alla pagina successiva o alla precedente in una sequenza di impaginazione, potresti voler far scorrere i contenuti in un'altra direzione, a seconda che tu stia passando alla pagina di livello superiore o inferiore della sequenza.

Registrazione della demo di paginazione. Utilizza transizioni diverse a seconda della pagina che stai visitando.

A questo scopo, puoi utilizzare i tipi di transizione di visualizzazione, che ti consentono di assegnare uno o più tipi a una transizione di visualizzazione attiva. Ad esempio, se passi a una pagina di livello superiore in una sequenza di impaginazione, utilizza il tipo forwards, mentre per passare a una pagina più bassa utilizza il tipo backwards. Questi tipi sono attivi solo durante l'acquisizione o l'esecuzione di una transizione e ogni tipo può essere personalizzato tramite CSS per utilizzare animazioni diverse.

Per utilizzare i tipi in una transizione di visualizzazione dello stesso documento, passa types al metodo startViewTransition. Per consentire questo, document.startViewTransition accetta anche un oggetto: update è la funzione di callback che aggiorna il DOM e types è un array con i tipi.

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});

Per rispondere a questi tipi, usa il selettore :active-view-transition-type(). Passa il type che vuoi scegliere come target nel selettore. In questo modo puoi mantenere gli stili di più transizioni di visualizzazione separati l'uno dall'altro, senza che le dichiarazioni della stessa interferiscano con le dichiarazioni dell'altra.

Poiché i tipi si applicano solo durante l'acquisizione o l'esecuzione della transizione, puoi utilizzare il selettore per impostare o annullare l'impostazione di un view-transition-name su un elemento solo per la transizione della visualizzazione con quel tipo.

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only (using the default root snapshot) */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

Nella seguente demo di impaginazione, i contenuti della pagina scorrono in avanti o indietro in base al numero di pagina a cui stai passando. I tipi sono determinati al clic con cui vengono trasmessi a document.startViewTransition.

Per scegliere come target qualsiasi transizione di visualizzazione attiva, indipendentemente dal tipo, puoi utilizzare il selettore di pseudo-classi :active-view-transition.

html:active-view-transition {
    
}

Gestire più stili di transizione della visualizzazione con un nome di classe nella radice della transizione della visualizzazione

A volte, una transizione da un particolare tipo di visualizzazione a un altro dovrebbe avere una transizione personalizzata. Oppure, una navigazione "indietro" dovrebbe essere diversa da una navigazione "in avanti".

Transizioni diverse quando torni indietro. Demo minima. Origine.

Prima dei tipi di transizione, il modo per gestire questi casi era impostare temporaneamente un nome classe nella radice della transizione. Quando chiami document.startViewTransition, questa radice di transizione è l'elemento <html>, accessibile utilizzando document.documentElement in JavaScript:

if (isBackNavigation) {
  document.documentElement.classList.add('back-transition');
}

const transition = document.startViewTransition(() =>
  updateTheDOMSomehow(data)
);

try {
  await transition.finished;
} finally {
  document.documentElement.classList.remove('back-transition');
}

Per rimuovere le classi al termine della transizione, questo esempio utilizza transition.finished, una promessa che si risolve quando la transizione ha raggiunto il suo stato finale. Le altre proprietà di questo oggetto sono trattate nel riferimento dell'API.

Ora puoi utilizzare il nome della classe nel CSS per modificare la transizione:

/* 'Forward' transitions */
::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
      cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
  animation-name: fade-out, slide-to-right;
}

.back-transition::view-transition-new(root) {
  animation-name: fade-in, slide-from-left;
}

Come per le media query, la presenza di queste classi potrebbe essere utilizzata anche per modificare gli elementi che ricevono un view-transition-name.


Eseguire transizioni senza bloccare altre animazioni

Dai un'occhiata a questa demo relativa a una posizione di transizione video:

Transizione video. Demo minima. Origine.

Hai notato qualche problema? Non preoccuparti se non l'hai fatto. Eccolo rallentato:

Transizione video, più lenta. Demo minima. Origine.

Durante la transizione, il video appare bloccarsi, poi la versione in riproduzione del video appare in dissolvenza. Questo perché ::view-transition-old(video) è uno screenshot della vecchia visualizzazione, mentre ::view-transition-new(video) è un'immagine in tempo reale della nuova visualizzazione.

Puoi risolvere il problema, ma prima chiediti se vale la pena farlo. Se non hai notato il "problema" quando la transizione veniva riprodotta alla velocità normale, non preoccuparti di cambiarla.

Se vuoi davvero correggerlo, non mostrare ::view-transition-old(video); passa direttamente a ::view-transition-new(video). Puoi farlo sostituendo gli stili e le animazioni predefiniti:

::view-transition-old(video) {
  /* Don't show the frozen old view */
  display: none;
}

::view-transition-new(video) {
  /* Don't fade the new view in */
  animation: none;
}

È tutto.

Transizione video, più lenta. Demo minima. Origine.

Ora il video viene riprodotto durante tutta la transizione.


Integrazione con l'API Navigation (e altri framework)

Le transizioni di visualizzazione vengono specificate in modo da poter essere integrate con altri framework o librerie. Ad esempio, se la tua applicazione a pagina singola (SPA) utilizza un router, puoi modificare il meccanismo di aggiornamento del router per aggiornare i contenuti utilizzando una transizione di visualizzazione.

Nel seguente snippet di codice tratto da questa demo di paginazione, il gestore di intercettazione dell'API Navigation è modificato in modo da chiamare document.startViewTransition quando le transizioni di visualizzazione sono supportate.

navigation.addEventListener("navigate", (e) => {
    // Don't intercept if not needed
    if (shouldNotIntercept(e)) return;

    // Intercept the navigation
    e.intercept({
        handler: async () => {
            // Fetch the new content
            const newContent = await fetchNewContent(e.destination.url, {
                signal: e.signal,
            });

            // The UA does not support View Transitions, or the UA
            // already provided a Visual Transition by itself (e.g. swipe back).
            // In either case, update the DOM directly
            if (!document.startViewTransition || e.hasUAVisualTransition) {
                setContent(newContent);
                return;
            }

            // Update the content using a View Transition
            const t = document.startViewTransition(() => {
                setContent(newContent);
            });
        }
    });
});

Alcuni browser, ma non tutti, forniscono la propria transizione quando l'utente esegue un gesto di scorrimento per navigare. In questo caso, non dovresti attivare la transizione di visualizzazione, in quanto ciò potrebbe comportare un'esperienza utente scadente o confusa. L'utente vedrà due transizioni, una fornita dal browser e l'altra da te, eseguite in successione.

Pertanto, è consigliabile impedire l'avvio di una transizione di visualizzazione quando il browser ha fornito la propria transizione visiva. Per farlo, controlla il valore della proprietà hasUAVisualTransition dell'istanza NavigateEvent. La proprietà è impostata su true se il browser ha fornito una transizione visiva. Questa proprietà hasUIVisualTransition esiste anche nelle istanze PopStateEvent.

Nello snippet precedente, il controllo che determina se eseguire la transizione di visualizzazione prende in considerazione questa proprietà. Quando non è supportata la transizione tra visualizzazioni dello stesso documento o quando il browser ha già fornito la propria transizione, la transizione della visualizzazione viene saltata.

if (!document.startViewTransition || e.hasUAVisualTransition) {
  setContent(newContent);
  return;
}

Nella registrazione seguente, l'utente scorre per tornare alla pagina precedente. Lo screenshot a sinistra non include un controllo per l'indicatore hasUAVisualTransition. La registrazione a destra include il controllo, quindi salta la transizione di visualizzazione manuale perché il browser ha fornito una transizione visiva.

Confronto dello stesso sito senza (a sinistra) e con larghezza (a destra) un controllo per hasUAVisualTransition

Animazione con JavaScript

Finora tutte le transizioni sono state definite utilizzando CSS, ma a volte CSS non è sufficiente:

Transizione della cerchia. Demo minima. Origine.

Due parti di questa transizione non possono essere ottenute solo con il CSS:

  • L'animazione inizia dalla posizione del clic.
  • L'animazione termina con il cerchio che ha un raggio fino all'angolo più lontano. Tuttavia, ci auguriamo che questo sarà possibile con CSS in futuro.

Fortunatamente, puoi creare transizioni utilizzando l'API Web Animation.

let lastClick;
addEventListener('click', event => (lastClick = event));

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // With a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

Questo esempio utilizza transition.ready, una promessa che si risolve una volta creati gli pseudo-elementi di transizione. Le altre proprietà di questo oggetto sono descritte nel riferimento API.


Le transizioni come miglioramento

L'API View Transition è progettata per "avvolgere" una modifica DOM e creare una transizione. Tuttavia, la transizione deve essere considerata un miglioramento, ovvero l'app non deve entrare in uno stato di "errore" se la modifica del DOM va a buon fine, ma la transizione non riesce. Idealmente, la transizione non dovrebbe fallire, ma se ciò accade, non deve interrompere il resto dell'esperienza utente.

Per trattare le transizioni come un miglioramento, fai attenzione a non utilizzare le promesse di transizione in modo che l'app non generi un errore se la transizione non va a buon fine.

Cosa non fare
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  await transition.ready;

  document.documentElement.animate(
    {
      clipPath: [`inset(50%)`, `inset(0)`],
    },
    {
      duration: 500,
      easing: 'ease-in',
      pseudoElement: '::view-transition-new(root)',
    }
  );
}

Il problema di questo esempio è che switchView() viene rifiutato se la transizione non riesce a raggiungere uno stato ready, ma questo non significa che il passaggio alla visualizzazione non sia riuscito. Il DOM potrebbe essere stato aggiornato correttamente, ma erano presenti view-transition-name duplicati, pertanto la transizione è stata saltata.

Invece:

Cosa fare
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  animateFromMiddle(transition);

  await transition.updateCallbackDone;
}

async function animateFromMiddle(transition) {
  try {
    await transition.ready;

    document.documentElement.animate(
      {
        clipPath: [`inset(50%)`, `inset(0)`],
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: '::view-transition-new(root)',
      }
    );
  } catch (err) {
    // You might want to log this error, but it shouldn't break the app
  }
}

In questo esempio viene utilizzato transition.updateCallbackDone per attendere l'aggiornamento del DOM e rifiutarlo se l'operazione non riesce. switchView non rifiuta più se la transizione non va a buon fine, si risolve al termine dell'aggiornamento del DOM e rifiuta se non va a buon fine.

Se vuoi che switchView venga risolto quando la nuova vista è "stabilita", ad esempio se qualsiasi transizione animata è stata completata o è saltata alla fine, sostituisci transition.updateCallbackDone con transition.finished.


Non è un polyfill, ma…

Non è una funzionalità facile da eseguire il polyfill. Tuttavia, questa funzione di supporto semplifica le cose nei browser che non supportano le transizioni della visualizzazione:

function transitionHelper({
  skipTransition = false,
  types = [],
  update,
}) {

  const unsupported = (error) => {
    const updateCallbackDone = Promise.resolve(update()).then(() => {});

    return {
      ready: Promise.reject(Error(error)),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
      types,
    };
  }

  if (skipTransition || !document.startViewTransition) {
    return unsupported('View Transitions are not supported in this browser');
  }

  try {
    const transition = document.startViewTransition({
      update,
      types,
    });

    return transition;
  } catch (e) {
    return unsupported('View Transitions with types are not supported in this browser');
  }
}

e può essere utilizzato in questo modo:

function spaNavigate(data) {
  const types = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    update() {
      updateTheDOMSomehow(data);
    },
    types,
  });

  // …
}

Nei browser che non supportano le transizioni di visualizzazione, updateDOM verrà comunque chiamato, ma non verrà visualizzata una transizione animata.

Puoi anche fornire alcuni classNames da aggiungere a <html> durante la transizione, in modo da modificare più facilmente la transizione a seconda del tipo di navigazione.

Puoi anche passare true a skipTransition se non vuoi un'animazione, anche nei browser che supportano le transizioni di visualizzazione. Questa opzione è utile se l'utente preferisce disattivare le transizioni il sito.


Utilizzo dei framework

Se utilizzi una libreria o un framework che esegue l'astrazione delle modifiche DOM, la parte difficile è sapere quando la modifica DOM è completata. Ecco un insieme di esempi che utilizzano l'helper riportato sopra in vari framework.

  • React: la chiave qui è flushSync, che applica in modo sincrono un insieme di modifiche dello stato. Sì, c'è un avviso importante sull'utilizzo di questa API, ma Dan Abramov mi assicura che è appropriata in questo caso. Come di consueto con React e il codice asincrono, quando utilizzi le varie promesse restituite da startViewTransition, assicurati che il codice venga eseguito con lo stato corretto.
  • Vue.js: la chiave qui è nextTick, che viene completata una volta aggiornato il DOM.
  • Svelte: molto simile a Vue, ma il metodo per attendere la modifica successiva è tick.
  • Lit: la chiave in questo caso è la promessa this.updateComplete all'interno dei componenti, che viene soddisfatta una volta aggiornato il DOM.
  • Angular: la chiave qui è applicationRef.tick, che esegue lo svuotamento delle modifiche DOM in attesa. A partire dalla versione 17 di Angular, puoi utilizzare withViewTransitions fornito con @angular/router.

Riferimento API

const viewTransition = document.startViewTransition(update)

Avvia un nuovo ViewTransition.

update è una funzione che viene chiamata dopo aver acquisito lo stato corrente del documento.

Se la promessa restituita da updateCallback viene soddisfatta, la transizione inizia nel frame successivo. Se la promessa restituita da updateCallback viene rifiutata, la transizione viene abbandonata.

const viewTransition = document.startViewTransition({ update, types })

Avvia un nuovo ViewTransition con i tipi specificati

update viene chiamato dopo aver acquisito lo stato corrente del documento.

types imposta i tipi attivi per la transizione quando la acquisisci o la esegui. È inizialmente vuoto. Per ulteriori informazioni, consulta viewTransition.types di seguito.

Membri dell'istanza di ViewTransition:

viewTransition.updateCallbackDone

Una promessa che viene soddisfatta quando la promessa restituita da updateCallback viene soddisfatta o rifiutata quando viene rifiutata.

L'API View Transition racchiude una modifica del DOM e crea una transizione. Tuttavia, a volte non ti interessa il successo o l'insuccesso dell'animazione di transizione, ma vorresti solo sapere se e quando avviene il cambiamento del DOM. updateCallbackDone è per quel caso d'uso.

viewTransition.ready

Una promessa che viene soddisfatta una volta creati gli pseudo-elementi per la transizione e l'animazione sta per iniziare.

Viene rifiutato se la transizione non può iniziare. Ciò può essere dovuto a una configurazione errata, ad esempio view-transition-name duplicati o al fatto che updateCallback restituisce una promessa rifiutata.

Questo è utile per animare gli pseudo-elementi di transizione con JavaScript.

viewTransition.finished

Una promessa che viene soddisfatta quando lo stato finale è completamente visibile e interattivo per l'utente.

L'azione viene rifiutata solo se updateCallback restituisce una promessa rifiutata, poiché questo indica che lo stato finale non è stato creato.

Altrimenti, se una transizione non inizia o viene saltata durante la transizione, lo stato finale viene comunque raggiunto, quindi finished viene completato.

viewTransition.types

Un oggetto simile a Set che contiene i tipi di transizione della visualizzazione attiva. Per manipolare le voci, utilizza i metodi di istanza clear(), add() e delete().

Per rispondere a un tipo specifico in CSS, utilizza il selettore della pseudo-classe :active-view-transition-type(type) nella radice della transizione.

I tipi vengono ripuliti automaticamente al termine della transizione di visualizzazione.

viewTransition.skipTransition()

Salta la parte di animazione della transizione.

La chiamata a updateCallback non verrà saltata, poiché la modifica del DOM è separata dalla transizione.


Riferimento allo stile e alla transizione predefiniti

::view-transition
L'elemento pseudo-radice che riempie l'area visibile e contiene ogni ::view-transition-group.
::view-transition-group

Posizionato in modo assoluto.

Transizioni width e height tra gli stati "prima" e "dopo".

Esegue la transizione di transform tra il quad "prima" e "dopo" per lo spazio dell'area visibile.

::view-transition-image-pair

Posizionato in modo assoluto per riempire il gruppo.

Ha isolation: isolate per limitare l'effetto del mix-blend-mode sulle visualizzazioni precedenti e nuove.

::view-transition-new e ::view-transition-old

Posizionato in modo assoluto in alto a sinistra del wrapper.

Riempie il 100% della larghezza del gruppo, ma ha un'altezza automatica, quindi manterrà le proporzioni anziché riempire il gruppo.

Ha mix-blend-mode: plus-lighter per consentire una dissolvenza incrociata reale.

La vecchia visualizzazione passa da opacity: 1 a opacity: 0. La nuova visualizzazione passa da opacity: 0 a opacity: 1.


Feedback

I feedback degli sviluppatori sono sempre apprezzati. A questo scopo, invia una segnalazione al CSS Working Group su GitHub con suggerimenti e domande. Prefissa il problema con [css-view-transitions].

Se riscontri un bug, segnala un bug di Chromium.