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:
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:
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:
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:
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:
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)
.
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.
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 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:
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 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?
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:
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.
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".
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:
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...
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 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(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 daupdateCallback
viene rifiutata, la transizione viene abbandonata.const viewTransition = document.startViewTransition({ update, types })
Inizia un nuovo
ViewTransition
con i tipi specificatiupdate
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. ConsultaviewTransition.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 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.types
Un oggetto simile a
Set
che contiene i tipi di transizione di Visualizzazione attiva. Per manipolare le voci, utilizza i relativi metodi di istanzaclear()
,add()
edelete()
.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
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 dimix-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
aopacity: 0
. La nuova visualizzazione passerà daopacity: 0
aopacity: 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.