Transizioni fluide e semplici con l'API View Transitions

Jake Archibald
Jake Archibald

Supporto dei browser

  • 111
  • 111
  • x
  • x

Fonte

L'API View Transizione semplifica la modifica del DOM in un solo passaggio, creando al contempo una transizione animata tra i due stati. È disponibile in Chrome 111 e versioni successive.

Transizioni create con l'API View Transizione. Prova il sito dimostrativo: è necessario Chrome 111 o versioni successive.

Perché abbiamo bisogno di questa funzionalità?

Le transizioni di pagina non solo hanno un aspetto fantastico, ma comunicano anche la direzione del flusso e indicano chiaramente quali elementi sono correlati da una pagina all'altra. Possono anche verificarsi durante il recupero dei dati, generando una percezione più rapida delle prestazioni.

Tuttavia, sul web sono già disponibili strumenti di animazione, come le transizioni CSS, le animazioni CSS e l'API di animazione web, quindi perché abbiamo bisogno di una nuova soluzione per spostare gli elementi?

La verità è che le transizioni di stato sono difficili, anche con gli strumenti di cui disponiamo già.

Anche una semplice dissolvenza incrociata comporta la presenza di entrambi gli stati contemporaneamente. Ciò presenta problemi di usabilità, come la gestione di interazioni aggiuntive sull'elemento in uscita. Inoltre, per gli utenti di dispositivi per la disabilità, c'è un periodo in cui sia lo stato "prima" che "dopo" sono nel DOM contemporaneamente e gli elementi potrebbero muoversi intorno all'albero in un modo che non è male visivamente, ma che possono facilmente causare la perdita del punto di lettura e della messa a fuoco.

La gestione delle modifiche dello stato è particolarmente difficile se i due stati differiscono nella posizione di scorrimento. Inoltre, se un elemento si sposta da un container all'altro, potresti incorrere in difficoltà con overflow: hidden e altre forme di ritaglio, il che significa che devi ristrutturare il tuo CSS per ottenere l'effetto desiderato.

Non è impossibile, è solo molto difficile.

La visualizzazione delle transizioni è un metodo più semplice perché ti consente di apportare modifiche nel DOM senza sovrapposizioni tra gli stati, ma di creare un'animazione di transizione tra gli stati utilizzando le visualizzazioni istantanee.

Inoltre, sebbene l'attuale implementazione abbia come target le app a pagina singola (APS), questa funzionalità verrà estesa per consentire le transizioni tra i caricamenti di pagine intere, cosa che al momento è impossibile.

Stato standardizzazione

La funzionalità è in fase di sviluppo all'interno del W3C CSS Working Group come bozza di specifica.

Quando saremo soddisfatti della progettazione dell'API, avvieremo i processi e i controlli necessari per rendere stabile questa funzionalità.

Il feedback degli sviluppatori è molto importante, quindi ti invitiamo a segnalare i problemi su GitHub con suggerimenti e domande.

La transizione più semplice: una dissolvenza incrociata

La transizione delle visualizzazioni predefinita è una dissolvenza incrociata, quindi è un'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 preferisci: aggiungi/rimuovi elementi, cambia i nomi delle classi, cambia gli stili... non importa.

In questo modo, le pagine presentano una dissolvenza incrociata:

La dissolvenza incrociata predefinita. Demo minima. Fonte.

Ok, una dissolvenza incrociata non è così impressionante. Fortunatamente, le transizioni possono essere personalizzate, ma prima di farlo dobbiamo capire come funzionava questa dissolvenza incrociata di base.

Come funzionano queste transizioni

Prendiamo l'esempio di codice riportato sopra:

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

Quando viene chiamato .startViewTransition(), l'API acquisisce lo stato attuale della pagina. Ciò include l'acquisizione di uno screenshot.

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

Una volta acquisito lo stato, l'API crea uno pseudo albero di pseudo-elementi come questo:

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

::view-transition si trova in un overlay, sopra tutto il resto della pagina. È 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 live della nuova visualizzazione. Entrambi vengono visualizzati come "contenuti sostituiti" CSS (come <img>).

La vista precedente si anima da opacity: 1 a opacity: 0, mentre la nuova vista da opacity: 0 a opacity: 1, creando una dissolvenza incrociata.

Tutte le animazioni vengono eseguite utilizzando animazioni CSS, che possono quindi essere personalizzate con CSS.

Personalizzazione semplice

Tutti gli pseudo-elementi sopra riportati 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 quell'unica modifica, la dissolvenza ora è molto lenta:

Dissolvenza incrociata lunga. Demo minima. Fonte.

Ok, ancora non è una cosa impressionante. Implementiamo 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. Fonte.

Transizione di più elementi

Nella demo precedente, l'intera pagina è coinvolta nella transizione dell'asse condiviso. Questo vale per la maggior parte della pagina, ma non sembra del tutto corretto per l'intestazione, perché scorre via per poi scorrere di nuovo all'interno.

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

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

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

Il risultato è che:

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

Ora l'intestazione rimane attiva e applica una dissolvenza incrociata.

Quella dichiarazione CSS ha causato la modifica della struttura di 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 sono disponibili due gruppi di transizione. Uno per l'intestazione e un altro per il resto. Il targeting di questi elementi può essere scelto in modo indipendente con il CSS e vengono applicate transizioni diverse. Tuttavia, in questo caso a main-header è rimasta la transizione predefinita, che è una dissolvenza incrociata.

Bene, la transizione predefinita non è solo una dissolvenza incrociata, ma anche ::view-transition-group transizioni:

  • Posiziona e trasforma (tramite un transform)
  • Larghezza
  • Altezza

Questo non è stato importante fino a questo momento, poiché l'intestazione ha le stesse dimensioni e la stessa posizione su entrambi i lati della modifica del DOM. Tuttavia, possiamo estrarre anche il testo presente nell'intestazione:

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

fit-content viene utilizzato in modo che l'elemento abbia le dimensioni del testo, anziché estenderlo alla larghezza rimanente. Senza questo, la freccia indietro riduce le dimensioni dell'elemento di testo di intestazione, mentre vogliamo che siano della stessa dimensione in entrambe le pagine.

Ora ci sono tre parti con cui giocare:

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

Ma ripeto le impostazioni predefinite:

Testo intestazione scorrevole. Demo minima. Fonte.

A questo punto, il testo dell'intestazione scorre per fare spazio al pulsante Indietro.

Debug delle transizioni

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

Con il riquadro Animazioni, puoi mettere in pausa l'animazione successiva e mandare avanti e indietro l'animazione. Durante questa operazione, gli pseudo-elementi di transizione si trovano nel riquadro Elementi.

Debug delle transizioni di visualizzazione con Chrome Dev Tools.

Gli elementi in 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 per il testo nell'intestazione. Sono concettualmente gli stessi elementi prima e dopo la modifica del DOM, ma puoi creare transizioni anche in questo caso.

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

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

Quindi, 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();
  });
};

Il risultato è:

Passaggio da un elemento a un altro. Demo minima. Fonte.

La miniatura diventa ora l'immagine principale. Anche se sono elementi concettualmente (e letteralmente) diversi, l'API di transizione li tratta come la stessa cosa perché condividevano lo stesso view-transition-name.

Il vero codice è un po' più complicato del semplice esempio riportato sopra, poiché gestisce anche la transizione alla pagina delle miniature. Consulta la fonte per l'implementazione completa.

Transizioni di entrata e uscita personalizzate

Guarda questo esempio:

Entrata e uscita dalla barra laterale. Demo minima. Fonte.

La barra laterale fa parte della transizione:

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

Tuttavia, a differenza dell'intestazione dell'esempio precedente, la barra laterale non viene visualizzata in 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 esiste un'immagine "vecchia" per la barra laterale, la coppia di immagini avrà solo un elemento ::view-transition-new(sidebar). Allo stesso modo, se la barra laterale si trova solo nella pagina precedente, la coppia di immagini avrà solo un ::view-transition-old(sidebar).

Nella demo precedente, la transizione della barra laterale varia a seconda che venga attivata, emessa o presente in entrambi gli stati. Entra scorrendo da destra e con la dissolvenza in entrata, esce scorrendo verso destra e con la dissolvenza in uscita. Rimane in posizione quando è presente in entrambi gli stati.

Per creare transizioni specifiche di entrata e uscita, puoi usare la pseudo-classe :only-child per scegliere come target lo pseudo-elemento vecchio/nuovo quando è l'unico elemento figlio 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 DOM asincroni e in attesa di contenuti

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

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

La transizione non inizierà finché non verrà soddisfatta la promessa. Durante questo periodo, la pagina è bloccata, pertanto i ritardi qui devono essere ridotti al minimo. In particolare, i recuperi di rete devono essere effettuati prima di chiamare .startViewTransition(), mentre la pagina è ancora completamente interattiva, anziché nell'ambito del callback .startViewTransition().

Se decidi di attendere che le immagini o i caratteri siano pronti, utilizza 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 questo ritardo e utilizzare i contenuti già in tuo possesso.

Come trarre il massimo dai contenuti di cui già disponi

Nel caso in cui la miniatura passi a un'immagine più grande:

La transizione predefinita è la dissolvenza incrociata, il che significa che la miniatura potrebbe presentare una dissolvenza incrociata con un'immagine intera 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 e sia possibile visualizzare una rotellina per indicare all'utente che gli elementi sono in fase di caricamento. Ma in questo caso c'è 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, si trova solo sotto l'immagine intera. Ciò significa che se la nuova visualizzazione non è stata caricata, la miniatura è visibile durante tutta la transizione. Ciò significa che la transizione può iniziare immediatamente e l'intera immagine può essere caricata a sua volta.

Ciò non funzionerebbe se la nuova vista mostrasse la trasparenza, ma in questo caso sappiamo che non è così, quindi possiamo effettuare questa ottimizzazione.

Gestione delle modifiche nelle proporzioni

Praticamente, finora tutte le transizioni hanno riguardato elementi con le stesse proporzioni, ma non sempre. Cosa succede se la miniatura è 1:1 e l'immagine principale è 16:9?

Passaggio da un elemento a un altro, con una modifica delle proporzioni. Demo minima. Fonte.

Nella transizione predefinita, il gruppo si anima dalla dimensione precedente a quella successiva. Le visualizzazioni vecchie e nuove hanno una larghezza del gruppo pari al 100% e l'altezza automatica, ovvero mantengono le proporzioni indipendentemente dalle dimensioni del gruppo.

Si tratta di un buon valore predefinito, ma non è quello che vogliamo 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 l'intera immagine viene "ritagliata" durante la transizione da 1:1 a 16:9.

Modifica della transizione in base allo stato del dispositivo

Ti consigliamo di utilizzare transizioni diverse sui dispositivi mobili e sui computer, come in questo esempio che mostra una slide intera di lato sui dispositivi mobili e una slide più discreta sui computer.

Passaggio da un elemento a un altro. Demo minima. Fonte.

Puoi ottenere questo risultato utilizzando query supporti regolari:

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

Puoi anche modificare gli elementi a cui assegni un view-transition-name a seconda delle query supporti corrispondenti.

Reazione alla preferenza "Movimento ridotto"

Gli utenti possono indicare che preferiscono il movimento ridotto tramite il proprio sistema operativo e questa preferenza viene esposta tramite 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 voglia nessun movimento. Al posto delle precedenti, potresti scegliere un'animazione più discreto, ma che esprima ancora la relazione tra gli elementi e il flusso di dati.

Modifica della transizione in base al tipo di navigazione

A volte, per passare da un determinato tipo di pagina a un'altra occorre effettuare una transizione su misura. Oppure, un elemento di navigazione "Indietro" dovrebbe essere diverso da uno "Avanti".

Transizioni diverse quando si torna indietro. Demo minima. Fonte.

Il modo migliore per gestire questi casi è impostare un nome di classe su <html>, noto anche come elemento del documento:

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

Questo esempio utilizza transition.finished, una promessa che si risolve una volta raggiunto lo stato finale della transizione. Altre proprietà di questo oggetto sono trattate nel riferimento API.

Ora puoi utilizzare il nome del corso 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 query supporti, la presenza di queste classi potrebbe essere utilizzata anche per modificare gli elementi che ricevono un view-transition-name.

Transizione senza bloccare le altre animazioni

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

Transizione video. Demo minima. Fonte.

Hai notato qualcosa che non va? In caso contrario, non preoccuparti. In questo caso, il processo è rallentato:

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

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

Puoi risolvere il problema, ma prima chiediti se vale la pena correggerlo. Se non vedessi il "problema" quando la transizione veniva riprodotta alla sua velocità normale, non mi preoccuperei di cambiarla.

Se vuoi davvero risolvere il problema, non mostrare ::view-transition-old(video); passa direttamente a ::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. Fonte.

Ora il video viene riprodotto durante 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. Fonte.

Un paio di parti di questa transizione non possono essere raggiunte solo con il CSS:

  • L'animazione parte dal punto del clic.
  • L'animazione termina con il cerchio che ha un raggio fino all'angolo più lontano. Tuttavia, speriamo che questo sarà possibile con CSS in futuro.

Per fortuna, puoi creare transizioni usando 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. Altre proprietà di questo oggetto sono trattate 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 trattata come un miglioramento, ad esempio perché l'app non deve avere uno stato "errore" se la modifica DOM ha esito positivo, ma la transizione non va a buon fine. Idealmente, la transizione non dovrebbe avere esito negativo, ma se dovesse accade, non dovrebbe interrompere il resto dell'esperienza utente.

Per considerare le transizioni come un miglioramento, fai attenzione a non utilizzare le promesse di transizione in un modo che potrebbe causare l'impatto della tua 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 di questo esempio è che switchView() rifiuterà se la transizione non può raggiungere lo stato ready, ma ciò non significa che il passaggio della visualizzazione non sia riuscito. Il DOM potrebbe essere stato aggiornato correttamente, ma erano presenti view-transition-name duplicati, quindi 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 per rifiutarlo in caso di errore. switchView non rifiuta più se la transizione non riesce, ma si risolve al completamento dell'aggiornamento DOM e la rifiuta in caso di errore.

Se vuoi che switchView venga risolto quando la nuova vista è stata "regolata", come nel caso di una transizione animata completata o saltata alla fine, sostituisci transition.updateCallbackDone con transition.finished.

Non è un polyfill, ma...

Non credo che questa funzione possa essere usata per il polyfill, ma sono felice che sia stata smentita.

Tuttavia, questa funzione helper semplifica notevolmente le operazioni nei browser che non supportano le transizioni di tipo visualizzazione:

function transitionHelper({
  skipTransition = false,
  classNames = [],
  updateDOM,
}) {
  if (skipTransition || !document.startViewTransition) {
    const updateCallbackDone = Promise.resolve(updateDOM()).then(() => {});

    return {
      ready: Promise.reject(Error('View transitions unsupported')),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
    };
  }

  document.documentElement.classList.add(...classNames);

  const transition = document.startViewTransition(updateDOM);

  transition.finished.finally(() =>
    document.documentElement.classList.remove(...classNames)
  );

  return transition;
}

E può essere usato nel seguente modo:

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

  const transition = transitionHelper({
    classNames,
    updateDOM() {
      updateTheDOMSomehow(data);
    },
  });

  // …
}

Nei browser che non supportano le transizioni delle visualizzazioni, 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 della transizione in base al tipo di navigazione.

Se non vuoi un'animazione, puoi anche passare true a skipTransition, anche nei browser che supportano le transizioni di tipo Visualizza. Questo è utile se un utente ha preferito disattivare le transizioni per il tuo sito.

Utilizzo dei framework

Se stai lavorando con una libreria o un framework che astrae le modifiche del DOM, la parte più difficile è sapere quando la modifica del DOM è stata completata. Ecco una serie di esempi, usando l'helper sopra, in vari framework.

  • Reazione: la chiave qui è flushSync, che applica un insieme di modifiche di stato in modo sincrono. Sì, esiste un avviso importante relativo all'uso di quell'API, ma Dan Abramov mi assicura che è appropriato in questo caso. Come di consueto con il codice React e asincrono, quando utilizzi le varie promesse restituite da startViewTransition, assicurati che il codice sia in esecuzione nello stato corretto.
  • Vue.js: la chiave qui è nextTick, che viene evasa una volta aggiornato il DOM.
  • Svelte: molto simile a Vue, ma il metodo per attendere la modifica successiva è tick.
  • Lit: 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 svuota le modifiche DOM in attesa. A partire dalla versione 17 di Angular, puoi utilizzare withViewTransitions fornito con @angular/router.

Riferimento API

const viewTransition = document.startViewTransition(updateCallback)

Inizia un nuovo ViewTransition.

updateCallback viene chiamato una volta acquisito lo stato attuale del documento.

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

Membri dell'istanza di ViewTransition:

viewTransition.updateCallbackDone

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

L'API View Transizione esegue il wrapping di una modifica DOM e crea una transizione. Tuttavia, a volte non ti interessa la riuscita o l'esito negativo dell'animazione della transizione; potresti soltanto sapere se e quando avviene la modifica del DOM. updateCallbackDone è per questo caso d'uso.

viewTransition.ready

Una promessa che si realizza 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 se 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.

Rifiuta 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.skipTransition()

Salta la parte dell'animazione della transizione.

Non verrà saltata la chiamata a updateCallback, perché la modifica del DOM è separata dalla transizione.

Stile predefinito e riferimento alle transizioni

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

Assolutamente posizionata.

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

Transizioni transform tra il riquadro area visibile "prima" e "dopo".

::view-transition-image-pair

In una posizione assoluta per riempire il gruppo.

Ha isolation: isolate per limitare l'effetto della modalità di fusione plus-lighter sulla vecchia e sulla nuova vista.

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

Assolutamente posizionata 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 passerà da opacity: 0 a opacity: 1.

Feedback

Il feedback degli sviluppatori è molto importante in questa fase, quindi invia una segnalazione di problemi su GitHub con suggerimenti e domande.