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

Quando una transizione di visualizzazione viene eseguita su un singolo documento, viene chiamata transizione della vista 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 dello 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 richiamato, il browser acquisisce automaticamente le istantanee di tutti gli elementi su cui è stata dichiarata una proprietà CSS view-transition-name.

Esegue quindi il callback passato in che aggiorna il DOM, dopodiché acquisisce gli snapshot del nuovo stato.

Queste istantanee vengono quindi disposte in una struttura di pseudo-elementi e animate utilizzando la potenza delle animazioni CSS. Coppie di istantanee del vecchio e del nuovo stato passano gradualmente dalla posizione e dalle dimensioni precedenti alla nuova posizione, mentre i contenuti si dissolveranno in dissolvenza. Se vuoi, puoi utilizzare CSS per personalizzare le animazioni.


Transizione predefinita: dissolvenza incrociata

La transizione di visualizzazione predefinita è una dissolvenza incrociata, quindi rappresenta una buona 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 nel nuovo stato. Puoi farlo come vuoi. Ad esempio, puoi aggiungere o rimuovere elementi, cambiare i nomi delle classi o cambiare gli stili.

E, in men che non si dica, le pagine hanno una dissolvenza incrociata:

La dissolvenza incrociata predefinita. Demo minima. Origine.

Ok, una dissolvenza incrociata non è così impressionante. Fortunatamente, le transizioni possono essere personalizzate, ma prima devi capire come funzionava questa dissolvenza incrociata 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 attuale della pagina. È inclusa l'acquisizione di un'istantanea.

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

Una volta acquisito il nuovo stato, l'API crea un albero di pseudo-elementi come il seguente:

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

::view-transition è posizionato in un overlay, su tutto il resto 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 vista si anima da opacity: 1 a opacity: 0, mentre la nuova vista si anima da opacity: 0 a opacity: 1, creando una dissolvenza incrociata.

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

Personalizza la transizione

Tutti gli pseudo-elementi di transizione della visualizzazione possono essere scelti come target con CSS e, poiché le animazioni sono definite tramite 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 unica modifica, la dissolvenza ora è molto lenta:

Dissolvenza incrociata lunga. Demo minima. Origine.

Ok, non è ancora impressionante. Il seguente codice 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;
}

Ecco il risultato:

Transizione asse condiviso. Demo minima. Origine.

Eseguire la transizione di più elementi

Nella demo precedente, l'intera pagina è coinvolta nella transizione dell'asse condiviso. Questo funziona per la maggior parte della pagina, ma non sembra del tutto appropriato per l'intestazione, dato che scorre in uscita per scorrere di nuovo.

Per evitare che ciò accada, puoi estrarre l'intestazione dal resto della pagina in modo da animarla separatamente. Per farlo, devi assegnare view-transition-name all'elemento.

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

Il valore di view-transition-name può essere qualsiasi cosa tu voglia (tranne che per none, che significa che non è presente alcun nome per la transizione). Viene utilizzato per identificare in modo univoco l'elemento nella transizione.

E il risultato:

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

Ora l'intestazione rimane ferma e fa dissolvenze incrociate.

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 possono essere scelti come target in modo indipendente con CSS e a loro volta assegnati diverse transizioni. Tuttavia, in questo caso main-header è stata lasciata con la transizione predefinita, che è 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 la dimensione dell'elemento di testo dell'intestazione, anziché la stessa dimensione in entrambe le pagine.

Ora abbiamo tre parti con cui giocare:

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

Ripeto, usando solo i valori predefiniti:

Testo dell'intestazione scorrevole. Demo minima. Origine.

Ora il testo dell'intestazione mostra una slide soddisfacente per fare spazio al pulsante Indietro.


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

Supporto dei browser

  • 125
  • 125
  • x
  • x

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 selettori che devi scrivere. Vuoi aggiungere un nuovo elemento? Poi devi anche ingrandire il selettore che applica gli stili di animazione. Non esattamente scalabile.

È possibile utilizzare view-transition-class negli pseudo-elementi delle transizioni di 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. Tutte le carte, incluse quelle appena aggiunte, vengono applicate allo stesso tempo con un unico selettore: html::view-transition-group(.card).

Registrazione della demo delle schede. Con l'utilizzo di view-transition-class, viene applicato lo stesso animation-timing-function a tutte le carte, ad eccezione di quelle aggiunte o rimosse.

Transizioni di debug

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 eseguire lo scrubbing avanti e indietro per tutta l'animazione. Durante questa operazione, gli pseudo-elementi di transizione si trovano nel riquadro Elementi.

Debug delle transizioni delle viste 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 al suo interno. Concettualmente, questi elementi sono gli stessi elementi prima e dopo il cambiamento del DOM, ma è possibile creare transizioni in cui non è così.

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

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

Quindi, quando l'utente fa clic sulla miniatura, può ricevere 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:

È in corso la transizione di un elemento a un altro. Demo minima. Origine.

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

Il codice reale per questa transizione è un po' più complicato rispetto all'esempio precedente, in quanto gestisce anche la transizione alla pagina in miniatura. Consulta la fonte 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 avranno 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 è presente una "vecchia" immagine 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 transizione della barra laterale varia a seconda che entri, esce o sia presente in entrambi gli stati. Entra facendo scorrere il dito da destra e con dissolvenza in entrata, esce facendo scorrere a destra e dissolvendo in uscita, e rimane in posizione quando è presente in entrambi gli stati.

Per creare transizioni di entrata e uscita specifiche, puoi utilizzare la pseudo-classe :only-child per scegliere come target gli pseudo-elementi vecchi o nuovi quando è l'unico elemento secondario 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, il che consente aggiornamenti asincroni del DOM e in attesa che i contenuti importanti siano pronti.

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

La transizione inizierà solo quando la promessa non verrà soddisfatta. Durante questo periodo, la pagina è bloccata, pertanto i ritardi devono essere ridotti al minimo. In particolare, i recuperi dalla rete devono essere eseguiti prima di chiamare .startViewTransition(), mentre la pagina è ancora completamente interattiva, invece di eseguirli come parte del callback .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 già in tuo possesso.


Sfrutta al massimo i contenuti che hai già

Nel caso in cui la miniatura si trasformi in un'immagine più grande:

Viene eseguita la transizione della miniatura 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 iniziare la transizione. Idealmente, questa operazione deve essere eseguita prima di chiamare .startViewTransition(), in modo che la pagina rimanga interattiva ed è possibile mostrare una rotellina per indicare all'utente che gli elementi vengono caricati. Ma in questo caso 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 svanisce, ma si limita 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 un secondo momento.

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

Comodamente, finora tutte le transizioni sono state effettuate verso elementi con le stesse proporzioni, ma non sempre. 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 si anima dalla dimensione precedente a quella successiva. Le visualizzazioni precedenti e nuove mostrano la larghezza del gruppo al 100% e l'altezza automatica, il che significa che mantengono le proporzioni indipendentemente dalle dimensioni del gruppo.

È una buona impostazione predefinita, ma non è quella desiderata 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, ma che l'immagine completa "un-ritaglio" passa da 1:1 a 16:9.

Per informazioni più dettagliate, consulta Visualizzare le transizioni: gestire le modifiche alle proporzioni


Utilizzare le query supporti per cambiare le transizioni per i diversi stati dei dispositivi

Potresti voler utilizzare transizioni diverse sui dispositivi mobili e sui computer, come in questo esempio che esegue una slide intera lateralmente sui dispositivi mobili, ma una slide più discreta su computer.

È in corso la transizione di un elemento 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;
  }
}

Potresti anche voler cambiare gli elementi che assegni a view-transition-name, a seconda delle query supporti corrispondenti.


Reagisci alla preferenza di "Movimento ridotto"

Gli utenti possono indicare di preferire la riduzione del movimento tramite il proprio sistema operativo e questa preferenza viene esposta 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. Invece dello 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 della visualizzazione con i tipi di transizione della visualizzazione

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 più in alto o più in basso dalla sequenza.

Registrazione della demo Impaginazione. Utilizza transizioni diverse a seconda della pagina che vuoi visitare.

A questo scopo, puoi utilizzare i tipi di transizione Visualizza, che consentono di assegnare uno o più tipi a una transizione 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 quando acquisisci o esegui 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 questa operazione, document.startViewTransition accetta anche un oggetto: update è la funzione di callback che aggiorna il DOM, mentre 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(). Inserisci nel selettore il valore type che vuoi scegliere come target. 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 paginazione, i contenuti della pagina scorrono in avanti o indietro in base al numero di pagina che stai visualizzando. 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 invece il selettore di pseudo-classe :active-view-transition.

html:active-view-transition {
    …
}

Gestisci più stili di transizione della visualizzazione con un nome di classe nella radice delle transizioni di 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 si torna in modalità "indietro". Demo minima. Origine.

Prima dei tipi di transizione, per gestire questi casi era necessario impostare temporaneamente un nome di classe nella radice di 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 una volta raggiunto lo stato di fine della transizione. Le altre proprietà di questo oggetto sono descritte nel riferimento API.

Ora puoi utilizzare quel nome di classe nel tuo 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 query supporti, la presenza di queste classi potrebbe essere utilizzata anche per cambiare gli elementi che ricevono un view-transition-name.


Eseguire transizioni senza bloccare le altre animazioni

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

Transizione video. Demo minima. Origine.

Hai notato qualcosa di sbagliato? In caso contrario, non preoccuparti. In questo caso il video è rallentato:

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

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

Puoi risolvere il problema, ma prima chiediti se vale la pena correggerlo. Se il "problema" non era stato rilevato mentre la transizione era in riproduzione alla velocità normale, non mi interessava modificarla.

Se vuoi davvero risolvere il problema, non mostrare ::view-transition-old(video); passa direttamente al ::view-transition-new(video). Per farlo, puoi sostituire 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;
}

e il gioco è fatto.

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

Ora il video viene riprodotto durante tutta la transizione.


Animazione con JavaScript

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

Transizione dalla cerchia. Demo minima. Origine.

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

  • L'animazione inizia dal punto del clic.
  • L'animazione termina con il cerchio che ha un raggio fino all'angolo più lontano. Tuttavia, spero che sarà possibile farlo 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 correttamente gli pseudo-elementi di transizione. Le altre proprietà di questo oggetto sono descritte nel riferimento API.


Transizioni come miglioramento

L'API View Transizione è progettata per eseguire il wrapping di una modifica DOM e creare una transizione corrispondente. Tuttavia, la transizione deve essere considerata come un miglioramento, perché la tua app non deve entrare in uno stato di "errore" se la modifica al DOM ha esito positivo, ma la transizione non va a buon fine. Idealmente la transizione non dovrebbe fallire, ma se succede, non dovrebbe interrompere il resto dell'esperienza utente.

Per considerare le transizioni come un miglioramento, fai attenzione a non usare le promesse di transizione in modo tale da impedire la visualizzazione dell'app 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 con questo esempio è che switchView() rifiuterà la transizione se la transizione non può raggiungere lo stato ready, ma questo non significa che il cambio di visualizzazione non sia riuscito. Il DOM potrebbe essere stato aggiornato correttamente, ma erano presenti view-transition-name duplicati, quindi la transizione è stata saltata.

Invece:

Che 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 completamento dell'aggiornamento del DOM e rifiuta se la transizione 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 si tratta di una funzionalità facile da usare per il polyfill. Tuttavia, questa funzione di supporto semplifica le cose nei browser che non supportano le transizioni delle visualizzazioni:

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 ci sarà una transizione animata.

Puoi anche fornire alcuni classNames da aggiungere a <html> durante la transizione, semplificando la modifica in base al tipo di navigazione.

Puoi anche passare da 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 stai lavorando con una libreria o un framework che astrae le modifiche del DOM, la parte difficile è sapere quando la modifica del DOM è completa. Ecco una serie di esempi, utilizzando l'helper sopra, in vari framework.

  • Reazione: la chiave in questo caso è flushSync, che applica un insieme di modifiche di stato in modo sincrono. Sì, esiste un avviso importante in merito all'utilizzo di quell'API, ma Dan Abramov mi assicura che è appropriata in questo caso. Come al solito con React e codice asincrono, quando utilizzi le varie promesse restituite da startViewTransition, assicurati che il codice sia in esecuzione 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 il cambiamento successivo è tick.
  • Lit: in questo caso la chiave è la promessa this.updateComplete all'interno dei componenti, che viene soddisfatta una volta aggiornato il DOM.
  • Angular: la chiave qui è applicationRef.tick, che esegue il flush delle modifiche al 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 una volta acquisito lo stato attuale 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 una volta acquisito lo stato attuale del documento.

types imposta i tipi attivi per la transizione durante l'acquisizione o l'esecuzione della transizione. All'inizio è vuoto. Consulta la sezione viewTransition.types più in basso per saperne di più.

Membri istanza di ViewTransition:

viewTransition.updateCallbackDone

Una promessa che si mantiene quando quella restituita da updateCallback viene soddisfatta o che si rifiuta quando la promessa viene rifiutata.

L'API View Transizione aggrega 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 questo caso d'uso.

viewTransition.ready

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

Se la transizione non può iniziare, viene rifiutata. 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 una volta che 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 di tipo Set che contiene i tipi di transizioni di Visualizzazione attiva. Per manipolare le voci, usa i metodi dell'istanza clear(), add() e delete().

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

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

viewTransition.skipTransition()

Salta la parte dell'animazione della transizione.

Non perderai la chiamata a updateCallback, poiché la modifica del DOM è separata dalla transizione.


Stile e riferimento predefiniti per le transizioni

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

Posizione esatta.

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

Assolutamente posizionato per riempire il gruppo.

isolation: isolate per limitare l'effetto di mix-blend-mode sulla vecchia e sulla nuova vista.

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

Posizionato assolutamente 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 vista precedente è passata da opacity: 1 a opacity: 0. La nuova visualizzazione viene modificata da opacity: 0 a opacity: 1.


Feedback

Il feedback degli sviluppatori è sempre apprezzato. A questo scopo, invia una segnalazione al CSS Working Group su GitHub con suggerimenti e domande. Fai precedere il problema da [css-view-transitions].

Se riscontri un bug, segnala un bug di Chromium.