Vlotte en eenvoudige overgangen met de View Transitions API

Browserondersteuning

  • 111
  • 111
  • X
  • X

Bron

Met de View Transition API kunt u de DOM eenvoudig in één stap wijzigen en tegelijkertijd een geanimeerde overgang tussen de twee statussen creëren. Het is beschikbaar in Chrome 111+.

Overgangen gemaakt met de View Transition API. Probeer de demosite – Vereist Chrome 111+.

Waarom hebben we deze functie nodig?

Paginaovergangen zien er niet alleen geweldig uit, ze communiceren ook de richting van de stroom en maken duidelijk welke elementen van pagina tot pagina gerelateerd zijn. Ze kunnen zelfs optreden tijdens het ophalen van gegevens, wat leidt tot een snellere perceptie van de prestaties.

Maar we hebben al animatietools op internet, zoals CSS-overgangen , CSS-animaties en de Web Animation API , dus waarom hebben we iets nieuws nodig om dingen te verplaatsen?

De waarheid is dat staatstransities moeilijk zijn, zelfs met de instrumenten die we al hebben.

Zelfs zoiets als een simpele cross-fade houdt in dat beide toestanden tegelijkertijd aanwezig zijn. Dat brengt uitdagingen op het gebied van bruikbaarheid met zich mee, zoals het omgaan met extra interactie op het uitgaande element. Voor gebruikers van hulpmiddelen is er ook een periode waarin zowel de voor- als de na-status zich tegelijkertijd in de DOM bevinden, en dingen door de boom kunnen bewegen op een manier die visueel prima is, maar er gemakkelijk voor kan zorgen dat de leespositie en focus verslechteren. verdwaald zijn.

Het omgaan met statuswijzigingen is vooral een uitdaging als de twee statussen verschillen in scrollpositie. En als een element van de ene container naar de andere beweegt, kunt u problemen ondervinden met overflow: hidden en andere vormen van clipping, wat betekent dat u uw CSS moet herstructureren om het gewenste effect te krijgen.

Het is niet onmogelijk, het is gewoon heel moeilijk .

Overgangen bekijken biedt u een eenvoudigere manier, doordat u uw DOM-wijziging kunt doorvoeren zonder enige overlap tussen staten, maar een overgangsanimatie tussen de staten kunt maken met behulp van momentopnamen.

Hoewel de huidige implementatie zich richt op single page apps (SPA's), zal deze functie bovendien worden uitgebreid om overgangen mogelijk te maken tussen het laden van volledige pagina's, wat momenteel onmogelijk is.

Standaardisatiestatus

De functie wordt binnen de W3C CSS Working Group ontwikkeld als conceptspecificatie .

Zodra we tevreden zijn met het API-ontwerp, starten we de processen en controles die nodig zijn om deze functie naar stable te verzenden.

Feedback van ontwikkelaars is erg belangrijk, dus plaats problemen op GitHub met suggesties en vragen.

De eenvoudigste overgang: een cross-fade

De standaard View Transition is een cross-fade, dus het dient als een leuke introductie tot de 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));
}

Waar updateTheDOMSomehow de DOM naar de nieuwe staat verandert. Dat kan op elke gewenste manier: elementen toevoegen/verwijderen, klassennamen wijzigen, stijlen wijzigen… het maakt niet uit.

En zomaar, pagina's vervagen:

De standaard cross-fade. Minimale demo . Bron .

Oké, een cross-fade is niet zo indrukwekkend. Gelukkig kunnen overgangen worden aangepast, maar voordat we daar aan toekomen, moeten we begrijpen hoe deze fundamentele cross-fade werkte.

Hoe deze transities werken

Het codevoorbeeld van bovenaf nemen:

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

Wanneer .startViewTransition() wordt aangeroepen, legt de API de huidige status van de pagina vast. Dit omvat het maken van een screenshot.

Zodra dat is voltooid, wordt de callback die wordt doorgegeven aan .startViewTransition() aangeroepen. Dat is waar de DOM wordt gewijzigd. Vervolgens legt de API de nieuwe status van de pagina vast.

Zodra de status is vastgelegd, construeert de API een boom met pseudo-elementen zoals deze:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

De ::view-transition bevindt zich in een overlay, over al het andere op de pagina. Dit is handig als u een achtergrondkleur voor de overgang wilt instellen.

::view-transition-old(root) is een screenshot van de oude weergave, en ::view-transition-new(root) is een live weergave van de nieuwe weergave. Beide worden weergegeven als CSS 'vervangen inhoud' (zoals een <img> ).

De oude weergave animeert van opacity: 1 naar opacity: 0 , terwijl de nieuwe weergave animeert van opacity: 0 naar opacity: 1 , waardoor een cross-fade ontstaat.

Alle animaties worden uitgevoerd met behulp van CSS-animaties, zodat ze kunnen worden aangepast met CSS.

Eenvoudig maatwerk

Alle bovenstaande pseudo-elementen kunnen worden getarget met CSS, en aangezien de animaties worden gedefinieerd met behulp van CSS, kunt u ze aanpassen met behulp van bestaande CSS-animatie-eigenschappen. Bijvoorbeeld:

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

Met die ene verandering is de vervaging nu erg langzaam:

Lange cross-fade. Minimale demo . Bron .

Oké, dat is nog steeds niet indrukwekkend. Laten we in plaats daarvan de gedeelde asovergang van Material Design implementeren:

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

En hier is het resultaat:

Gedeelde asovergang. Minimale demo . Bron .

Meerdere elementen overzetten

In de vorige demo was de hele pagina betrokken bij de gedeelde asovergang. Dat werkt voor het grootste deel van de pagina, maar het lijkt niet helemaal juist voor de kop, omdat deze naar buiten schuift om vervolgens weer naar binnen te schuiven.

Om dit te voorkomen, kunt u de koptekst uit de rest van de pagina halen, zodat deze afzonderlijk kan worden geanimeerd. Dit wordt gedaan door een view-transition-name aan het element toe te wijzen.

.main-header {
  view-transition-name: main-header;
}

De waarde van view-transition-name kan zijn wat u maar wilt (behalve none , wat betekent dat er geen transitienaam is). Het wordt gebruikt om het element tijdens de overgang op unieke wijze te identificeren.

En het resultaat daarvan:

Gedeelde asovergang met vaste header. Minimale demo . Bron .

Nu blijft de header op zijn plaats en vervaagt hij.

Die CSS-declaratie zorgde ervoor dat de boom met pseudo-elementen veranderde:

::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)

Er zijn nu twee transitiegroepen. Eén voor de header en één voor de rest. Deze kunnen onafhankelijk worden getarget met CSS en met verschillende overgangen. Hoewel in dit geval main-header de standaardovergang had, wat een cross-fade is.

Nou, oké, de standaardovergang is niet alleen een cross-fade, de ::view-transition-group gaat ook over:

  • Positioneren en transformeren (via een transform )
  • Breedte
  • Hoogte

Dat deed er tot nu toe niet toe, omdat de header dezelfde grootte heeft en dezelfde positie heeft aan beide zijden van de DOM-wijziging. Maar we kunnen ook de tekst in de koptekst extraheren:

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

fit-content wordt gebruikt zodat het element de grootte van de tekst heeft, in plaats van zich uit te rekken tot de resterende breedte. Zonder dit verkleint de pijl-terug de grootte van het koptekstelement, terwijl we willen dat deze op beide pagina's dezelfde grootte heeft.

Dus nu hebben we drie delen om mee te spelen:

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

Maar nogmaals, gewoon met de standaardinstellingen gaan:

Glijdende koptekst. Minimale demo . Bron .

Nu schuift de koptekst een beetje bevredigend over om ruimte te maken voor de terugknop.

Debuggen van overgangen

Omdat weergaveovergangen bovenop CSS-animaties zijn gebouwd, is het deelvenster Animaties in Chrome DevTools ideaal voor het opsporen van fouten in overgangen.

Met behulp van het deelvenster Animaties kunt u de volgende animatie pauzeren en vervolgens heen en weer door de animatie bladeren. Tijdens deze periode zijn de overgangspseudo-elementen te vinden in het paneel Elementen .

Fouten opsporen in weergaveovergangen met Chrome Dev Tools.

Overgangselementen hoeven niet hetzelfde DOM-element te zijn

Tot nu toe hebben we view-transition-name gebruikt om afzonderlijke overgangselementen voor de koptekst en de tekst in de koptekst te maken. Dit zijn conceptueel hetzelfde element voor en na de DOM-wijziging, maar je kunt overgangen maken waar dat niet het geval is.

De hoofdvideo-insluiting kan bijvoorbeeld een view-transition-name krijgen:

.full-embed {
  view-transition-name: full-embed;
}

Als er vervolgens op de miniatuur wordt geklikt, kan deze dezelfde view-transition-name krijgen, alleen voor de duur van de overgang:

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

En het resultaat:

Het ene element gaat over in het andere. Minimale demo . Bron .

De miniatuur gaat nu over in de hoofdafbeelding. Hoewel het conceptueel (en letterlijk) verschillende elementen zijn, behandelt de transitie-API ze als hetzelfde omdat ze dezelfde view-transition-name delen.

De echte code hiervoor is iets ingewikkelder dan het eenvoudige voorbeeld hierboven, omdat deze ook de overgang terug naar de miniatuurpagina afhandelt. Zie de bron voor de volledige implementatie.

Aangepaste in- en uitstapovergangen

Kijk naar dit voorbeeld:

Zijbalk openen en verlaten. Minimale demo . Bron .

De zijbalk maakt deel uit van de overgang:

.sidebar {
  view-transition-name: sidebar;
}

Maar in tegenstelling tot de koptekst in het vorige voorbeeld verschijnt de zijbalk niet op alle pagina's. Als beide staten de zijbalk hebben, zien de overgangspseudo-elementen er als volgt uit:

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

Als de zijbalk zich echter alleen op de nieuwe pagina bevindt, zal het pseudo-element ::view-transition-old(sidebar) er niet zijn. Omdat er geen 'oude' afbeelding voor de zijbalk is, heeft het afbeeldingspaar alleen een ::view-transition-new(sidebar) . Op dezelfde manier, als de zijbalk zich alleen op de oude pagina bevindt, heeft het afbeeldingspaar alleen een ::view-transition-old(sidebar) .

In de bovenstaande demo gaat de zijbalk anders over, afhankelijk van of deze in beide staten binnenkomt, verlaat of aanwezig is. Het komt binnen door van rechts te schuiven en naar binnen te faden, het komt naar buiten door naar rechts te schuiven en uit te faden, en het blijft op zijn plaats als het in beide toestanden aanwezig is.

Om specifieke entry- en exit-overgangen te maken, kunt u de :only-child pseudo-klasse gebruiken om het oude/nieuwe pseudo-element te targeten wanneer dit het enige kind in het afbeeldingspaar is:

/* 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 dit geval is er geen specifieke overgang voor wanneer de zijbalk in beide staten aanwezig is, omdat de standaard perfect is.

Asynchrone DOM-updates en wachten op inhoud

De callback die wordt doorgegeven aan .startViewTransition() kan een belofte retourneren, die asynchrone DOM-updates mogelijk maakt en wacht tot belangrijke inhoud gereed is.

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

De transitie zal pas beginnen als de belofte wordt waargemaakt. Gedurende deze tijd is de pagina bevroren, dus vertragingen moeten hier tot een minimum worden beperkt. Netwerkophaalacties moeten met name worden uitgevoerd voordat .startViewTransition() wordt aangeroepen, terwijl de pagina nog steeds volledig interactief is, in plaats van ze uit te voeren als onderdeel van de callback .startViewTransition() .

Als u besluit te wachten totdat afbeeldingen of lettertypen gereed zijn, zorg er dan voor dat u een agressieve time-out gebruikt:

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

In sommige gevallen is het echter beter om de vertraging helemaal te vermijden en de inhoud te gebruiken die u al heeft.

Haal het beste uit de inhoud die u al heeft

In het geval dat de miniatuur overgaat in een grotere afbeelding:

De standaardovergang is cross-fade, wat betekent dat de miniatuur kan cross-faden met een nog niet geladen volledige afbeelding.

Eén manier om dit aan te pakken is door te wachten tot de volledige afbeelding is geladen voordat u met de overgang begint. Idealiter zou dit worden gedaan voordat .startViewTransition() wordt aangeroepen, zodat de pagina interactief blijft en er een spinner kan worden weergegeven om aan de gebruiker aan te geven dat dingen worden geladen. Maar in dit geval is er een betere manier:

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

Nu vervaagt de miniatuur niet, maar bevindt deze zich gewoon onder de volledige afbeelding. Dit betekent dat als de nieuwe weergave niet is geladen, de miniatuur zichtbaar is tijdens de overgang. Dit betekent dat de overgang meteen kan beginnen en dat de volledige afbeelding op zijn eigen tijd kan worden geladen.

Dit zou niet werken als de nieuwe weergave transparantie zou bieden, maar in dit geval weten we dat dit niet het geval is, dus kunnen we deze optimalisatie doorvoeren.

Omgaan met veranderingen in de beeldverhouding

Handig genoeg waren alle overgangen tot nu toe naar elementen met dezelfde beeldverhouding, maar dat zal niet altijd het geval zijn. Wat als de miniatuur 1:1 is en de hoofdafbeelding 16:9?

Het ene element gaat over naar het andere, met een verandering in de beeldverhouding. Minimale demo . Bron .

Bij de standaardovergang animeert de groep van de voor- naar de na-grootte. De oude en nieuwe weergaven zijn 100% breed en automatisch hoog, wat betekent dat ze hun beeldverhouding behouden, ongeacht de groepsgrootte.

Dit is een goede standaard, maar in dit geval is dit niet wat we willen. Dus:

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

Dit betekent dat de miniatuur in het midden van het element blijft naarmate de breedte groter wordt, maar dat de volledige afbeelding wordt 'uitgesneden' wanneer deze overgaat van 1:1 naar 16:9.

De overgang wijzigen afhankelijk van de apparaatstatus

Mogelijk wilt u verschillende overgangen gebruiken op mobiel versus desktop, zoals dit voorbeeld, waarbij op mobiel een volledige dia vanaf de zijkant wordt uitgevoerd, maar een subtielere dia op desktop:

Het ene element gaat over in het andere. Minimale demo . Bron .

Dit kan worden bereikt met behulp van reguliere mediaquery's:

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

Mogelijk wilt u ook wijzigen aan welke elementen u een view-transition-name toewijst, afhankelijk van overeenkomende mediaquery's.

Reageren op de voorkeur voor 'verminderde beweging'

Gebruikers kunnen via hun besturingssysteem aangeven dat ze minder beweging willen, en die voorkeur wordt via CSS zichtbaar .

U kunt ervoor kiezen om eventuele overgangen voor deze gebruikers te voorkomen:

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Een voorkeur voor 'verminderde beweging' betekent echter niet dat de gebruiker geen beweging wil. In plaats van het bovenstaande zou je een subtielere animatie kunnen kiezen, maar wel één die nog steeds de relatie tussen de elementen en de gegevensstroom tot uitdrukking brengt.

De overgang wijzigen afhankelijk van het type navigatie

Soms moet een navigatie van het ene type pagina naar het andere een specifiek op maat gemaakte overgang hebben. Of een 'terug'-navigatie moet anders zijn dan een 'vooruit'-navigatie.

Verschillende overgangen bij het ‘teruggaan’. Minimale demo . Bron .

De beste manier om deze gevallen af ​​te handelen is door een klassenaam in te stellen op <html> , ook wel het documentelement genoemd:

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

In dit voorbeeld wordt transition.finished gebruikt, een belofte die wordt vervuld zodra de transitie de eindstatus heeft bereikt. Andere eigenschappen van dit object worden behandeld in de API-referentie .

Nu kunt u die klassenaam in uw CSS gebruiken om de overgang te wijzigen:

/* '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;
}

Net als bij mediaquery's kan de aanwezigheid van deze klassen ook worden gebruikt om te wijzigen welke elementen een view-transition-name krijgen.

Overstappen zonder andere animaties te bevriezen

Bekijk deze demo van een video-overgangspositie:

Video-overgang. Minimale demo . Bron .

Heb je er iets verkeerds aan gezien? Maak je geen zorgen als je dat niet hebt gedaan. Hier wordt het vertraagd:

Video-overgang, langzamer. Minimale demo . Bron .

Tijdens de overgang lijkt de video vast te lopen, waarna de afgespeelde versie van de video verdwijnt. Dit komt omdat de ::view-transition-old(video) een screenshot is van de oude weergave, terwijl de ::view-transition-new(video) is een live beeld van de nieuwe weergave.

Je kunt dit oplossen, maar vraag jezelf eerst af of het de moeite waard is om het te repareren. Als je het 'probleem' niet zou zien toen de overgang op normale snelheid speelde, zou ik niet de moeite nemen om deze te veranderen.

Als je het echt wilt repareren, laat dan niet de ::view-transition-old(video) ; schakel direct naar de ::view-transition-new(video) . U kunt dit doen door de standaardstijlen en animaties te overschrijven:

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

En dat is het!

Video-overgang, langzamer. Minimale demo . Bron .

Nu wordt de video tijdens de overgang afgespeeld.

Animeren met JavaScript

Tot nu toe zijn alle overgangen gedefinieerd met behulp van CSS, maar soms is CSS niet voldoende:

Cirkel overgang. Minimale demo . Bron .

Een aantal onderdelen van deze transitie kunnen niet alleen met CSS worden bereikt:

  • De animatie begint vanaf de kliklocatie.
  • De animatie eindigt met de cirkel met een straal naar de verste hoek. Hoewel dit hopelijk in de toekomst mogelijk zal zijn met CSS.

Gelukkig kun je overgangen maken met de Web Animation API !

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)',
      }
    );
  });
}

In dit voorbeeld wordt transition.ready gebruikt, een belofte die wordt opgelost zodra de pseudo-elementen voor de transitie met succes zijn gemaakt. Andere eigenschappen van dit object worden behandeld in de API-referentie .

Overgangen als verbetering

De View Transition API is ontworpen om een ​​DOM-wijziging 'in te pakken' en er een transitie voor te maken. De overgang moet echter worden behandeld als een verbetering, omdat uw app geen 'fout'-status mag krijgen als de DOM-wijziging slaagt, maar de overgang mislukt. Idealiter mag de transitie niet mislukken, maar als dat wel het geval is, mag dit de rest van de gebruikerservaring niet verstoren.

Als u overgangen als een verbetering wilt beschouwen, moet u ervoor zorgen dat u de overgangsbeloften niet op een manier gebruikt die ervoor zou zorgen dat uw app een foutmelding krijgt als de overgang mislukt.

Niet doen
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)',
    }
  );
}

Het probleem met dit voorbeeld is dat switchView() zal afwijzen als de transitie de status ready niet kan bereiken, maar dat betekent niet dat de weergave er niet in is geslaagd om te schakelen. De DOM is mogelijk succesvol bijgewerkt, maar er waren dubbele view-transition-name , dus de overgang werd overgeslagen.

In plaats van:

Doen
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 dit voorbeeld wordt transition.updateCallbackDone gebruikt om te wachten op de DOM-update en om te weigeren als deze mislukt. switchView weigert niet langer als de transitie mislukt, maar wordt opgelost wanneer de DOM-update is voltooid en wordt afgewezen als deze mislukt.

Als u wilt switchView wordt opgelost wanneer de nieuwe weergave is 'gesetteld', zoals wanneer een geanimeerde overgang is voltooid of naar het einde is overgeslagen, vervangt u transition.updateCallbackDone door transition.finished .

Geen polyfill, maar...

Ik denk niet dat deze functie op een nuttige manier kan worden ingevuld, maar ik ben blij dat ik ongelijk krijg!

Deze helperfunctie maakt het echter veel eenvoudiger in browsers die geen weergaveovergangen ondersteunen:

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

En het kan als volgt worden gebruikt:

function spaNavigate(data) {
  const classNames = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    classNames,
    updateDOM() {
      updateTheDOMSomehow(data);
    },
  });

  // …
}

In browsers die View Transitions niet ondersteunen, wordt updateDOM nog steeds aangeroepen, maar is er geen geanimeerde overgang.

U kunt ook enkele classNames opgeven die u tijdens de transitie aan <html> kunt toevoegen, waardoor het gemakkelijker wordt om de transitie te wijzigen, afhankelijk van het type navigatie .

U kunt ook true doorgeven om skipTransition slaan als u geen animatie wilt, zelfs in browsers die View Transitions ondersteunen. Dit is handig als uw site een gebruikersvoorkeur heeft om overgangen uit te schakelen.

Werken met kaders

Als je met een bibliotheek of raamwerk werkt dat DOM-wijzigingen abstraheert, is het lastig om te weten wanneer de DOM-wijziging voltooid is. Hier is een reeks voorbeelden, waarbij gebruik wordt gemaakt van de bovenstaande helper , in verschillende raamwerken.

  • Reageren : de sleutel hier is flushSync , die een reeks statuswijzigingen synchroon toepast. Ja, er is een grote waarschuwing over het gebruik van die API, maar Dan Abramov verzekert mij dat dit in dit geval gepast is. Zoals gebruikelijk bij React en asynchrone code, zorg er bij het gebruik van de verschillende beloften die worden geretourneerd door startViewTransition voor dat uw code in de juiste staat wordt uitgevoerd.
  • Vue.js —de sleutel hier is nextTick , die wordt vervuld zodra de DOM is bijgewerkt.
  • Svelte - lijkt erg op Vue, maar de methode om op de volgende wijziging te wachten is tick .
  • Lit : de sleutel hier is de this.updateComplete belofte binnen componenten, die wordt vervuld zodra de DOM is bijgewerkt.
  • Angular : de sleutel hier is applicationRef.tick , waarmee lopende DOM-wijzigingen worden gewist. Vanaf Angular versie 17 kunt u withViewTransitions gebruiken dat wordt meegeleverd met @angular/router .

API-referentie

const viewTransition = document.startViewTransition(updateCallback)

Start een nieuwe ViewTransition .

updateCallback wordt aangeroepen zodra de huidige status van het document is vastgelegd.

Wanneer de door updateCallback geretourneerde belofte wordt vervuld, begint de overgang in het volgende frame. Als de door updateCallback geretourneerde belofte wordt afgewezen, wordt de transitie afgebroken.

Instantieleden van ViewTransition :

viewTransition.updateCallbackDone

Een belofte die wordt nagekomen wanneer de door updateCallback geretourneerde belofte wordt nagekomen, of wordt afgewezen wanneer deze wordt afgewezen.

De View Transition API verpakt een DOM-wijziging en creëert een transitie. Soms maakt het succes/mislukken van de overgangsanimatie u echter niets uit, maar wilt u gewoon weten of en wanneer de DOM-wijziging plaatsvindt. updateCallbackDone is voor dat gebruik.

viewTransition.ready

Een belofte die werkelijkheid wordt zodra de pseudo-elementen voor de transitie zijn gecreëerd en de animatie op het punt staat te beginnen.

Het wijst het af als de transitie niet kan beginnen. Dit kan te wijten zijn aan een verkeerde configuratie, zoals dubbele view-transition-name , of als updateCallback een afgewezen belofte retourneert.

Dit is handig voor het animeren van de overgangspseudo-elementen met JavaScript .

viewTransition.finished

Een belofte die wordt waargemaakt zodra de eindtoestand volledig zichtbaar en interactief is voor de gebruiker.

Het wijst alleen af ​​als updateCallback een afgewezen belofte retourneert, omdat dit aangeeft dat de eindstatus niet is gemaakt.

Anders, als een transitie niet op gang komt, of wordt overgeslagen tijdens de transitie, wordt de eindtoestand nog steeds bereikt, dus finished is vervuld.

viewTransition.skipTransition()

Sla het animatiegedeelte van de overgang over.

Hiermee wordt het aanroepen van updateCallback niet overgeslagen, omdat de DOM-wijziging losstaat van de overgang.

Standaardstijl en overgangsreferentie

::view-transition
Het root-pseudo-element dat de viewport vult en elke ::view-transition-group bevat.
::view-transition-group

Absoluut gepositioneerd.

Overgangen width en height tussen de 'voor' en 'na' staten.

Overgangen transform tussen de 'voor' en 'na' viewport-ruimte quad.

::view-transition-image-pair

Absoluut gepositioneerd om de groep te vullen.

Heeft isolation: isolate om het effect van de plus-lighter overvloeimodus op de oude en nieuwe weergaven te beperken.

::view-transition-new en ::view-transition-old

Absoluut linksboven op de verpakking gepositioneerd.

Vult 100% van de groepsbreedte, maar heeft een automatische hoogte, zodat de beeldverhouding behouden blijft in plaats van de groep te vullen.

Heeft mix-blend-mode: plus-lighter om een ​​echte cross-fade mogelijk te maken.

De oude weergave gaat over van opacity: 1 naar opacity: 0 . De nieuwe weergave gaat over van opacity: 0 naar opacity: 1 .

Feedback

Feedback van ontwikkelaars is in deze fase erg belangrijk, dus plaats problemen op GitHub met suggesties en vragen.