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.
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:
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:
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 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:
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:
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.
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 è:
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:
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?
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.
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".
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:
Hai notato qualcosa che non va? In caso contrario, non preoccuparti. In questo caso, il processo è rallentato:
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.
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:
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.
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:
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 dastartViewTransition
, 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 utilizzarewithViewTransitions
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 daupdateCallback
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 seupdateCallback
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
eheight
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 fusioneplus-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
aopacity: 0
. La nuova visualizzazione passerà daopacity: 0
aopacity: 1
.
Feedback
Il feedback degli sviluppatori è molto importante in questa fase, quindi invia una segnalazione di problemi su GitHub con suggerimenti e domande.