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

Quando una transizione della visualizzazione viene eseguita su un singolo documento, prende il nome di transizione della visualizzazione dello stesso documento. Ciò si verifica in genere nelle applicazioni a pagina singola (APS) in cui JavaScript viene utilizzato per aggiornare il DOM. Le transizioni della visualizzazione dello stesso documento sono supportate in Chrome a partire dalla versione 111.

Per attivare una transizione per la 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 per i quali è stata dichiarata una proprietà CSS view-transition-name.

Quindi esegue il callback passato che aggiorna il DOM, dopodiché acquisisce le istantanee del nuovo stato.

Queste istantanee vengono poi disposte in un albero di pseudo-elementi e animate grazie alla potenza delle animazioni CSS. Coppie di istantanee dal vecchio e nuovo stato passano senza problemi dalla posizione e dalle dimensioni precedenti a quella nuova, mentre i contenuti si dissolvenza in modo incrociato. Se vuoi, puoi utilizzare CSS per personalizzare le animazioni.


La transizione predefinita: Dissolvenza incrociata

La transizione di visualizzazione predefinita è una dissolvenza incrociata, quindi serve da una bella 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. Ad esempio, puoi aggiungere o rimuovere elementi, cambiare i nomi delle classi o cambiare gli stili.

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 è necessario capire come funzionava questa dissolvenza incrociata di base.


Come funzionano queste transizioni

Aggiorniamo l'esempio di codice precedente.

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

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

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 il nuovo 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.

Personalizza la transizione

Tutti gli pseudo-elementi di transizione della vista possono essere scelti come target con CSS e, poiché le animazioni sono definite mediante 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. Invece, il seguente codice implementa 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.

Eseguire la 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 (utilizzando 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, puoi anche estrarre 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 questi elementi, la freccia indietro riduce le dimensioni dell'elemento di testo di intestazione, piuttosto che la 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.


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 una serie 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 aumentare il selettore che applica gli stili di animazione. Non esattamente scalabile.

È possibile utilizzare view-transition-class negli pseudo-elementi di transizione della vista 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);
}

Il seguente esempio di schede utilizza lo snippet CSS precedente. A tutte le schede, incluse quelle appena aggiunte, viene applicato lo stesso tempo con un selettore: html::view-transition-group(.card).

Registrazione della demo delle schede. Utilizzando view-transition-class, viene applicato lo stesso animation-timing-function a tutte le carte tranne quelle aggiunte o rimosse.

Esegui il debug delle transizioni

Poiché le transizioni delle visualizzazioni si basano sulle animazioni CSS, il riquadro Animazioni di 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.

Eseguire il debug delle transizioni delle visualizzazioni con Chrome DevTools.

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 per questa transizione è un po' più complicato dell'esempio precedente, 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 cambia a seconda che venga entrata, 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 gli pseudo-elementi vecchi o nuovi 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.


Sfrutta al meglio i contenuti di cui già disponi

Nel caso in cui la miniatura passi 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 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.

Gestire le 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 desiderato 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.

Per informazioni più dettagliate, visita la pagina "Visualizzare le transizioni: gestire le modifiche alle proporzioni" (https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/)


Utilizza le query supporti per modificare le transizioni per i diversi stati dei dispositivi

Ti consigliamo di utilizzare transizioni diverse sui dispositivi mobili rispetto a quelle 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.


Reagisci alla preferenza "Movimento ridotto"

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


Gestire più stili di transizione di visualizzazione con tipi di transizione di visualizzazione

A volte, una transizione da una particolare vista a un'altra deve avere una transizione su misura. Ad esempio, quando passi alla pagina successiva o precedente in una sequenza di impaginazione, potresti voler far scorrere i contenuti in una direzione diversa a seconda che tu stia andando a una pagina superiore o a una pagina inferiore rispetto alla sequenza.

Registrazione della demo della paginazione. Utilizza transizioni diverse a seconda della pagina che visiti.

A questo scopo, puoi utilizzare i tipi di transizione di visualizzazione, che consentono di assegnare uno o più tipi a una transizione di Visualizzazione attiva. Ad esempio, quando si passa a una pagina superiore in una sequenza di impaginazione, si usa il tipo forwards, mentre quando si passa a una pagina inferiore si usa il tipo backwards. Questi tipi sono attivi solo quando si acquisisce o si esegue una transizione e ogni tipo può essere personalizzato tramite CSS per l'utilizzo di animazioni diverse.

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

const direction = determineBackwardsOrForwards();

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

Per rispondere a questi tipi, utilizza il selettore :active-view-transition-type(). Passa il valore type che vuoi scegliere come target nel selettore. In questo modo, puoi mantenere separati tra loro gli stili di più transizioni di visualizzazione, senza che le dichiarazioni di una di esse interferiscano con le dichiarazioni dell'altra.

Poiché i tipi si applicano solo quando acquisisci o esegui la 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 avanti o indietro in base al numero di pagina a cui stai accedendo. I tipi vengono determinati al clic che 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 {
    …
}

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

A volte, il passaggio da un determinato tipo di vista a un altro richiede 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.

Prima dei tipi di transizione, il modo per gestire questi casi era impostare temporaneamente un nome di classe sulla radice di transizione. Quando chiami document.startViewTransition, questa radice di transizione è l'elemento <html>, accessibile tramite 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 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.


Esegui le transizioni 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:

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 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...

Questa funzionalità non è facile da usare per il polyfill. Tuttavia, questa funzione helper semplifica notevolmente le operazioni nei browser che non supportano le transizioni di tipo 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 usato nel seguente 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, verrà comunque chiamato updateDOM, 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 visualizzazione. 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(update)

Inizia un nuovo ViewTransition.

update è una funzione che viene chiamata dopo l'acquisizione dello 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.

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

Inizia 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 al momento dell'acquisizione o dell'esecuzione della transizione. Inizialmente è vuoto. Consulta viewTransition.types più in basso per ulteriori informazioni.

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 il successo o il fallimento dell'animazione della transizione non ti interessano; potresti semplicemente 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.types

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

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

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

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 di mix-blend-mode sulle viste vecchie e nuove.

::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 è sempre apprezzato. Per farlo, invia una segnalazione al CSS Working Group su GitHub con suggerimenti e domande. Aggiungi il prefisso [css-view-transitions] al problema.

Se riscontri un bug, segnala un bug di Chromium.