L'avvio di una transizione di visualizzazione con document.startViewTransition() associa l'overlay ::view-transition all'elemento :root, il che blocca gli eventi del puntatore sul resto della pagina, impedisce le transizioni simultanee e oscura le intestazioni position: fixed o gli elementi [popover] aperti (anche quando vengono utilizzati gruppi di transizioni di visualizzazione nidificati per impedire l'overflow).
La chiamata di element.startViewTransition() su una radice del componente risolve questo problema limitando l'ambito della transizione a quel sottoalbero DOM: l'albero di pseudo-elementi ::view-transition viene inserito direttamente nell'elemento, preservando i contesti di impilamento esterni e l'interattività della pagina, consentendo al contempo la transizione simultanea di più componenti.
Implementazione
1. Attiva una transizione con ambito
Chiama element.startViewTransition() sull'elemento contenitore principale del componente anziché su document.startViewTransition(). L'elemento su cui chiami startViewTransition() diventa l'ambito della transizione (l'elemento di ambito): vengono acquisiti solo gli elementi all'interno del suo sottoalbero, solo il rendering dell'ambito si interrompe durante il callback di aggiornamento e l'overlay ::view-transition risultante corrisponde alle dimensioni, alla forma e al contesto di impilamento dell'elemento di ambito.
2. Nome degli elementi partecipanti
Assegna un view-transition-name agli elementi secondari all'interno dell'ambito che devono essere animati. Puoi utilizzare view-transition-name: match-element sugli elementi di un elenco dinamico per ricavare automaticamente il nome della transizione di ogni elemento dall'identità del nodo DOM o assegnare nomi espliciti (che devono essere univoci solo all'interno di quell'ambito).
I nomi sono isolati all'interno di ogni ambito, evitando conflitti tra i componenti in diverse parti del DOM. Tuttavia, i view-transition-name possono entrare in conflitto con i nomi nelle transizioni di visualizzazione con ambito nidificato. Per evitare che ciò accada, aggiungi view-transition-scope: all all'elemento di ambito secondario.
3. Partecipazione personale e clipping dell'overflow
L'elemento scope partecipa automaticamente alla propria transizione di visualizzazione perché il foglio di stile dello user agent applica view-transition-name: root e view-transition-group: contain. In questo modo, quando la geometria dell'ambito viene influenzata dalla modifica apportata nel DOM, la sua geometria viene animata in modo fluido insieme ai suoi elementi secondari, nell'ambito della transizione di visualizzazione.
Quando l'ambito ritaglia l'overflow (overflow: hidden, scroll o clip), ::view-transition-group-children(root) applica automaticamente overflow: clip, impedendo agli elementi secondari in transizione di fuoriuscire dai limiti del componente.
4. Transizioni simultanee e nidificate
I sottoalberi dei componenti separati possono eseguire element.startViewTransition() contemporaneamente senza saltarsi a vicenda. Gli ambiti attivi ricevono automaticamente view-transition-scope: all nel foglio di stile UA, in modo che i valori view-transition-name interni non vengano divulgati nelle transizioni esterne. Applica view-transition-scope: all in CSS ai contenitori di componenti ripetuti per impedire alle transizioni esterne di acquisire i nomi interni anche quando il componente è inattivo.
Se una transizione esterna deve spostare un componente mentre la transizione interna è attiva, assegna un view-transition-name a un elemento che racchiude l'ambito interno. La transizione esterna acquisisce e anima solo il wrapper anziché l'elemento di ambito interno, consentendo l'esecuzione della transizione interna fino al completamento.
Esempio
Non è necessario alcun CSS sull'elemento di ambito stesso (.card o .card ul) per stabilire l'ambito, che viene determinato chiamando startViewTransition() sull'elemento anziché document (se il componente verrà nidificato all'interno di una transizione di visualizzazione esterna, consulta Esecuzione di una transizione esterna per view-transition-scope: all).
/* 1. Name participating child items */
.card ul li {
view-transition-name: match-element;
}
/* 2. OPTIONAL: Customize transition timing (300ms is an example duration) */
::view-transition-group(*) {
animation-duration: 300ms;
animation-timing-function: ease-in-out;
}
/* 3. MANDATORY: Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
// 1. Trigger a transition scoped to a single component subtree
function runScopedTransition(scopeEl, updateDOM) {
// MANDATORY: Call startViewTransition on the component element, not document
const transition = scopeEl.startViewTransition(() => {
updateDOM();
});
// Re-triggering startViewTransition() on the same element skips the active transition
// and rejects `ready`. Catch the rejection to avoid unhandled promise rejection errors.
transition.ready.catch(() => {});
}
// 2. Reorder items by moving existing DOM nodes (required for `match-element`)
// Scope to `listEl` since only the list order changes, keeping controls outside `listEl` interactive
function reorderItems(listEl, orderedIds) {
const existing = new Map(
Array.from(listEl.children, (li) => [li.dataset.id, li]),
);
runScopedTransition(listEl, () => {
for (const id of orderedIds) {
const li = existing.get(id);
if (li) listEl.appendChild(li);
}
});
}
// 3. Remove an item while preserving exit animation and keyboard focus
// Scope to the outer `cardEl` so the card itself also transitions its height change
function removeItem(cardEl, itemEl) {
const hadFocus = itemEl.contains(document.activeElement);
const nextFocusTarget =
itemEl.nextElementSibling?.querySelector('button') ??
itemEl.previousElementSibling?.querySelector('button') ??
cardEl.querySelector('button');
runScopedTransition(cardEl, () => {
itemEl.remove();
if (hadFocus) {
nextFocusTarget?.focus();
}
});
}
Esecuzione di una transizione esterna
Quando si avvia una transizione di visualizzazione al di fuori dell'ambito previsto (ad esempio da un componente principale o dal documento per una transizione a livello di pagina), la transizione esterna acquisisce e anima l'elemento dell'ambito e tutto ciò che contiene.
Per evitare che ciò accada, inserisci l'elemento di ambito in un elemento wrapper separato con il proprio view-transition-name e applica view-transition-scope: all all'elemento di ambito interno. In questo modo, il wrapper stesso può essere acquisito e animato dalla transizione esterna, impedendo al contempo l'acquisizione dei contenuti dell'ambito interno.
<div class="page" id="page">
<div class="card-wrapper">
<section class="card" id="left">
<h2>Card A</h2>
<ul id="left-list">
<li>…</li>
</ul>
</section>
</div>
<div class="card-wrapper">
<section class="card" id="right">
<h2>Card B</h2>
<ul id="right-list">
<li>…</li>
</ul>
</section>
</div>
</div>
document.querySelector('#page').startViewTransition(() => {
// do your DOM updates here...
});
/* Apply a view-transition-name on the wrapper */
.card-wrapper {
view-transition-name: match-element;
}
/* Prevent the contents of the card being captured by an outer transition */
.card {
view-transition-scope: all;
}
.card ul li {
view-transition-name: match-element;
}
In alternativa, puoi consentire alla transizione esterna di acquisire gli elementi secondari. Per mantenere gli effetti di ritaglio, utilizza i gruppi di transizioni di visualizzazione nidificati per ritagliare gli elementi secondari acquisiti ai confini del loro wrapper. In questo caso, però, devi applicare manualmente clip allo pseudo ::view-transition-group-children() risultante.
.card-wrapper {
overflow: clip;
view-transition-name: match-element; /* Capture me as part of the outer VT */
view-transition-group: contain; /* Nest children that also have a view-transition-name */
view-transition-class: card; /* For targeting purposes */
}
/* Manually copy back the clip onto the ::view-transition-group-children pseudo */
::view-transition-group-children(.card) {
overflow: clip;
}
.card ul li {
view-transition-name: match-element;
}
Vincoli e accessibilità
- OBBLIGATORIO: chiama
startViewTransition()sull'elemento root del componente anziché sudocumentin modo che gli elementiposition: fixedesterni e gli overlay[popover]rimangano sovrapposti e il resto della pagina rimanga interattivo. - OBBLIGATORIO: conserva e sposta i nodi DOM esistenti (anziché ricrearli con
innerHTML) quando utilizziview-transition-name: match-element, poichématch-elementmonitora l'identità dei nodi DOM. - OBBLIGATORIO: rispetta le preferenze dell'utente per il movimento ridotto utilizzando
@media (prefers-reduced-motion: reduce)impostandoanimation: none !importantsu::view-transition-group(*),::view-transition-old(*)e::view-transition-new(*). - OBBLIGATORIO: mantieni la selezione della tastiera quando rimuovi un elemento selezionato durante una transizione spostando la selezione su un elemento o un controllo adiacente all'interno del callback di aggiornamento.
- SÌ: utilizza i gruppi di transizione di visualizzazione nidificati (
view-transition-group: containeoverflow: clipsu::view-transition-group-children()) quando un elemento acquisito all'interno dell'ambito ritaglia il proprio overflow e contiene elementi acquisiti nidificati. - SÌ: collega
transition.ready.catch(() => {})quando interazioni rapide possono attivare nuovamenteelement.startViewTransition()sullo stesso elemento di ambito. - DA FARE: se una transizione esterna deve animare la posizione del componente mentre è in esecuzione una transizione interna, inserisci un elemento di ambito interno in un elemento wrapper con nome separato.
- DA EVITARE: imposta
view-transition-name: nonesu un elemento di ambito che ritaglia l'overflow (overflow: hidden,scrolloclip) o modifica la propria geometria, poiché la disattivazione della partecipazione automatica disattiva il ritaglio automatico dell'overflow.
Strategie di riserva
Se il tuo target Baseline non supporta le transizioni di visualizzazione con ambito elemento, esegui il downgrade controllato eseguendo immediatamente il callback di aggiornamento del DOM senza animazione. Sebbene sia possibile utilizzare document.startViewTransition(), questa opzione non è consigliata per gli aggiornamenti di componenti isolati, poiché una transizione con ambito documento blocca le transizioni di componenti simultanee e oscura gli elementi position: fixed e [popover] nel livello superiore.
// Feature detect specifically on Element.prototype (not document.startViewTransition)
const supportsScopedTransition = 'startViewTransition' in Element.prototype;
function runWithTransition(scopeEl, updateDOM) {
if (!supportsScopedTransition) {
// Fallback: apply the DOM update immediately without animation
updateDOM();
return;
}
const transition = scopeEl.startViewTransition(() => {
updateDOM();
});
transition.ready.catch(() => {});
}
Se hai davvero bisogno di un fallback utilizzando document.startViewTransition(), devi assicurarti che gli effetti di ritaglio dei container vengano preservati, come descritto nella sezione Esecuzione di una transizione esterna. Tieni presente che questo approccio di riserva interrompe la possibilità di eseguire più transizioni di visualizzazione contemporaneamente e oscura gli elementi position: fixed e [popover] nel livello superiore.