Overgangen van componenten met bereik

Als je een weergaveovergang start met document.startViewTransition(), wordt de ::view-transition-overlay aan het :root-element gekoppeld. Hierdoor worden aanwijzergebeurtenissen op de rest van de pagina geblokkeerd, worden gelijktijdige overgangen voorkomen en worden position: fixed-headers of open [popover]-elementen verborgen (zelfs als er geneste weergaveovergangsgroepen worden gebruikt om overloop te voorkomen).

Als je element.startViewTransition() aanroept op een componentroot, wordt dit opgelost door de overgang te beperken tot die DOM-substructuur: de ::view-transition-pseudoelementstructuur wordt rechtstreeks in het element geïnjecteerd, waardoor externe stapelcontexten en pagina-interactiviteit behouden blijven, terwijl meerdere componenten tegelijkertijd kunnen overgaan.

Implementatie

1. Een overgang met bereik activeren

Roep element.startViewTransition() aan voor het rootcontainerelement van de component in plaats van document.startViewTransition(). Het element waarop je startViewTransition() aanroept, wordt het bereik van de overgang (het bereikelement): alleen elementen in de substructuur worden vastgelegd, alleen de weergave van het bereik wordt gestopt tijdens de update-callback en de resulterende ::view-transition-overlay komt overeen met de grootte, vorm en stapelcontext van het bereikelement.

2. Naam van deelnemende elementen

Wijs een view-transition-name toe aan onderliggende elementen binnen het bereik dat moet worden geanimeerd. Je kunt view-transition-name: match-element gebruiken voor dynamische lijstitems om de overgangsnaam van elk element automatisch af te leiden van de DOM-node-identiteit of expliciete namen toe te wijzen (die alleen uniek hoeven te zijn binnen dat bereik).

Namen worden binnen elk bereik geïsoleerd, waardoor conflicten tussen componenten in verschillende delen van de DOM worden voorkomen. view-transition-name's kunnen wel botsen met namen in geneste overgangen van deelweergaven. Voeg view-transition-scope: all toe aan het onderliggende bereikelement om dit te voorkomen.

3. Zelfdeelname en overloopclipping

Het bereikelement neemt automatisch deel aan de eigen weergaveovergang omdat de stylesheet van de user-agent view-transition-name: root en view-transition-group: contain erop toepast. Als de geometrie van het bereik wordt beïnvloed door de wijziging die je aanbrengt in de DOM, animeert het op deze manier de eigen geometrie soepel naast de onderliggende elementen, als onderdeel van de weergaveovergang.

Als het bereik de overloop afkapt (overflow: hidden, scroll of clip), past ::view-transition-group-children(root) automatisch overflow: clip toe. Zo wordt voorkomen dat onderliggende elementen buiten de grenzen van de component vallen.

4. Gelijktijdige en geneste overgangen

Afzonderlijke subtrees van componenten kunnen element.startViewTransition() gelijktijdig uitvoeren zonder elkaar over te slaan. Actieve bereiken krijgen automatisch view-transition-scope: all in de UA-stylesheet, zodat interne view-transition-name-waarden niet lekken naar buitenste overgangen. Pas view-transition-scope: all toe in CSS op herhaalde componentcontainers om te voorkomen dat externe overgangen interne namen vastleggen, zelfs als de component inactief is.

Als een buitenste overgang een component moet verplaatsen terwijl de binnenste overgang actief is, wijs je een view-transition-name toe aan een element dat het binnenste bereik omhult. De buitenste overgang legt dan alleen de wrapper vast en animeert deze in plaats van het binnenste bereikelement, zodat de binnenste overgang kan worden voltooid.

Voorbeeld

Er is geen CSS nodig voor het bereikelement zelf (.card of .card ul) om het bereik vast te stellen. Het bereik wordt bepaald door startViewTransition() aan te roepen voor het element in plaats van document (als de component wordt genest in een buitenste weergaveovergang, ga je naar Een buitenste overgang uitvoeren voor 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();
    }
  });
}

Een buitenste overgang uitvoeren

Als je een weergaveovergang start buiten het beoogde bereik (bijvoorbeeld vanuit een bovenliggende component of vanuit het document voor een overgang op paginaniveau), legt de buitenste overgang het bereikelement en alles daarin vast en animeert deze.

Als je dit wilt voorkomen, plaats je het bereikelement in een afzonderlijk wrapper-element met een eigen view-transition-name en pas je view-transition-scope: all toe op het interne bereikelement. Hierdoor kan de wrapper zelf worden vastgelegd en geanimeerd door de buitenste overgang, terwijl wordt voorkomen dat de content van het binnenste bereik wordt vastgelegd.

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

Je kunt ook toestaan dat de buitenste overgang de onderliggende elementen vastlegt. Als je knipeffecten wilt behouden, gebruik je geneste weergaveovergangsgroepen om de vastgelegde onderliggende elementen aan de grenzen van hun wrapper te knippen. In dat geval moet je de clip handmatig toepassen op de resulterende ::view-transition-group-children()-pseudocode.

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

Beperkingen en toegankelijkheid

  • VERPLICHT: Roep startViewTransition() aan op het rootelement van de component in plaats van document, zodat externe position: fixed-elementen en [popover]-overlays gelaagd blijven en de rest van de pagina interactief blijft.
  • VERPLICHT: Bewaar en verplaats bestaande DOM-nodes (in plaats van ze opnieuw te maken met innerHTML) als je view-transition-name: match-element gebruikt, omdat match-element de identiteit van DOM-nodes bijhoudt.
  • VERPLICHT: Respecteer de gebruikersvoorkeuren voor minder beweging met @media (prefers-reduced-motion: reduce) door animation: none !important in te stellen op ::view-transition-group(*), ::view-transition-old(*) en ::view-transition-new(*).
  • VERPLICHT: Behoud de toetsenbordfocus als je een element met focus verwijdert tijdens een overgang door de focus te verplaatsen naar een aangrenzend item of bedieningselement in de update-callback.
  • WEL: Gebruik geneste weergaveovergangsgroepen (view-transition-group: contain en overflow: clip op ::view-transition-group-children()) als een vastgelegd element binnen het bereik de eigen overloop afkapt en geneste vastgelegde elementen bevat.
  • WEL: Voeg transition.ready.catch(() => {}) toe als snelle interacties element.startViewTransition() opnieuw kunnen activeren voor hetzelfde bereikelement.
  • WEL DOEN: Plaats een intern bereikelement in een afzonderlijk benoemd wrapper-element als een externe overgang de positie van de component moet animeren terwijl een interne overgang wordt uitgevoerd.
  • DOE DIT NIET: Stel view-transition-name: none in op een bereikelement dat de overloop afkapt (overflow: hidden, scroll of clip) of de eigen geometrie wijzigt, omdat afmelden voor zelfdeelname automatische overloopafkapping uitzet.

Reservestrategieën

Als je Baseline-doel geen weergaveovergangen met elementbereik ondersteunt, kun je de DOM-update-callback onmiddellijk uitvoeren zonder animatie. Je kunt terugvallen op document.startViewTransition(), maar dit wordt niet aanbevolen voor geïsoleerde componentupdates, omdat een overgang met documentbereik gelijktijdige componentovergangen blokkeert en position: fixed- en [popover]-elementen in de bovenste laag verbergt.

// 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(() => {});
}

Als je echt een reserveoptie met document.startViewTransition() nodig hebt, zorg je dat de bijsnijdeffecten van containers behouden blijven, zoals beschreven in het gedeelte Een buitenste overgang uitvoeren. Deze fallback-benadering maakt het onmogelijk om meerdere weergaveovergangen tegelijk uit te voeren en verbergt position: fixed- en [popover]-elementen in de bovenste laag.