Weergaveovergangen van hetzelfde document voor toepassingen met één pagina

Gepubliceerd: 17 aug. 2021, Laatst bijgewerkt: 25 sep. 2024

Wanneer een weergaveovergang op één document wordt uitgevoerd, wordt dit een 'same-document view transition' genoemd. Dit is meestal het geval in single-page applicaties (SPA's) waar JavaScript wordt gebruikt om de DOM bij te werken. Overgangen naar dezelfde documentweergave worden in Chrome ondersteund vanaf Chrome 111.

Om een ​​overgang naar dezelfde documentweergave te activeren, roept u document.startViewTransition aan:

function handleClick(e) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow();
    return;
  }

  // With a View Transition:
  document.startViewTransition(() => updateTheDOMSomehow());
}

Wanneer deze optie wordt aangeroepen, maakt de browser automatisch momentopnamen van alle elementen waaraan de CSS-eigenschap view-transition-name is gedeclareerd.

Vervolgens wordt de meegegeven callback uitgevoerd die de DOM bijwerkt, waarna er snapshots worden gemaakt van de nieuwe status.

Deze snapshots worden vervolgens gerangschikt in een boomstructuur van pseudo-elementen en geanimeerd met behulp van CSS-animaties. Paren snapshots van de oude en nieuwe staat gaan vloeiend over van hun oude positie en grootte naar hun nieuwe locatie, terwijl hun inhoud overvloeit. Je kunt de animaties desgewenst aanpassen met CSS.


De standaardovergang: Cross-fade

De standaardweergaveovergang is een cross-fade, wat een mooie introductie tot de API oplevert:

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. Je kunt bijvoorbeeld elementen toevoegen of verwijderen, klassennamen wijzigen of stijlen aanpassen.

En zo gaan de pagina's in elkaar over:

De standaard cross-fade. Minimale demo . Bron .

Oké, een cross-fade is niet zo indrukwekkend. Gelukkig kun je overgangen aanpassen, maar eerst moet je begrijpen hoe deze basis-cross-fade werkt.


Hoe deze overgangen werken

Laten we het vorige codevoorbeeld bijwerken.

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

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

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

Zodra de nieuwe status is vastgelegd, construeert de API een pseudo-elementboom 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 je 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 'vervangende 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 overgang ontstaat.

Alle animaties worden uitgevoerd met behulp van CSS-animaties en kunnen dus met CSS worden aangepast.

Pas de overgang aan

Alle pseudo-elementen van de weergaveovergang kunnen met CSS worden aangestuurd. Omdat de animaties met CSS worden gedefinieerd, kunt u ze aanpassen met bestaande CSS-animatie-eigenschappen. Bijvoorbeeld:

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

Door die ene verandering verloopt het vervagen nu heel langzaam:

Lange crossfade. Minimale demo . Bron .

Oké, dat is nog steeds niet indrukwekkend. In plaats daarvan implementeert de volgende code de gedeelde asovergang van 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;
}

En dit is het resultaat:

Gedeelde asovergang. Minimale demo . Bron .

Overgang van meerdere elementen

In de vorige demo is de hele pagina betrokken bij de gedeelde asovergang. Dat werkt voor het grootste deel van de pagina, maar het lijkt niet helemaal te passen bij de kop, omdat die naar buiten schuift om er vervolgens weer in te schuiven.

Om dit te voorkomen, kunt u de header uit de rest van de pagina halen, zodat deze afzonderlijk kan worden geanimeerd. Dit doet u 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 elke gewenste waarde zijn (behalve none , wat betekent dat er geen overgangsnaam is). Deze waarde wordt gebruikt om het element in de overgang eenduidig ​​te identificeren.

En het resultaat daarvan:

Gedeelde asovergang met vaste header. Minimale demo . Bron .

Nu blijft de header op zijn plaats en vervaagt deze.

Die CSS-declaratie zorgde ervoor dat de pseudo-elementboom 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 overgangsgroepen: één voor de header en één voor de rest. Deze kunnen onafhankelijk van elkaar met CSS worden aangestuurd en verschillende overgangen krijgen. In dit geval bleef de standaardovergang, een cross-fade, echter behouden voor main-header .

Oké, de standaardovergang is niet alleen een cross-fade, de ::view-transition-group maakt ook overgangen:

  • Positie en transformatie (met behulp van een transform )
  • Breedte
  • Hoogte

Dat maakte tot nu toe niet uit, omdat de header dezelfde grootte heeft en de positie aan beide kanten van de DOM verandert. Maar je kunt de tekst in de header ook 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 over de resterende breedte. Zonder deze functie verkleint de pijl-terug de grootte van het headertekstelement, in plaats van dezelfde grootte op beide pagina's.

Nu hebben we dus drie onderdelen om mee te spelen:

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

Maar nogmaals, ik blijf bij de standaardinstellingen:

Schuivende koptekst. Minimale demo . Bron .

Nu schuift de koptekst op een prettige manier over de pagina om ruimte te maken voor de terugknop.


Animeer meerdere pseudo-elementen op dezelfde manier met view-transition-class

Browser Support

  • Chroom: 125.
  • Rand: 125.
  • Firefox Technology Preview: ondersteund.
  • Safari: 18.2.

Stel je voor dat je een weergaveovergang hebt met een aantal kaarten, maar ook een titel op de pagina. Om alle kaarten behalve de titel te animeren, moet je een selector schrijven die elke afzonderlijke kaart activeert.

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

Heb je 20 elementen? Dan moet je 20 selectors schrijven. Voeg je een nieuw element toe? Dan moet je ook de selector die de animatiestijlen toepast uitbreiden. Niet echt schaalbaar.

De view-transition-class kan worden gebruikt in de pseudo-elementen van de view-overgang om dezelfde stijlregel toe te passen.

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

Het volgende kaartvoorbeeld maakt gebruik van het vorige CSS-fragment. Alle kaarten, inclusief nieuw toegevoegde kaarten, krijgen dezelfde timing met één selector: html::view-transition-group(.card) .

Opname van de kaartendemo . Met behulp van view-transition-class wordt dezelfde animation-timing-function toegepast op alle kaarten, behalve de toegevoegde of verwijderde.

Debug-overgangen

Omdat weergaveovergangen worden opgebouwd op basis van CSS-animaties, is het paneel Animaties in Chrome DevTools ideaal voor het opsporen van fouten in overgangen.

Met het paneel Animaties kun je de volgende animatie pauzeren en vervolgens heen en weer door de animatie scrollen. De pseudo-elementen voor de overgangen vind je in het paneel Elementen .

Weergaveovergangen debuggen met Chrome DevTools.

Overgangselementen hoeven niet hetzelfde DOM-element te zijn

Tot nu toe hebben we view-transition-name gebruikt om aparte overgangselementen te maken voor de header en de tekst in de header. Dit zijn in principe dezelfde elementen vóór en na de DOM-wijziging, maar je kunt overgangen maken waar dat niet het geval is.

U kunt bijvoorbeeld aan de belangrijkste video-embed een view-transition-name geven:

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

Wanneer u vervolgens op de miniatuur klikt, kunt u deze dezelfde view-transition-name geven, maar dan voor de duur van de overgang:

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

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

En het resultaat:

Eén element dat overgaat in een ander. 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 voor deze overgang is iets ingewikkelder dan in het vorige voorbeeld, omdat deze ook de overgang terug naar de thumbnailpagina verwerkt. Zie de broncode voor de volledige implementatie.


Aangepaste in- en uitgangsovergangen

Kijk eens naar dit voorbeeld:

Zijbalk openen en sluiten. Minimale demo . Bron .

De zijbalk is onderdeel van de overgang:

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

Maar in tegenstelling tot de header in het vorige voorbeeld, verschijnt de zijbalk niet op alle pagina's. Als beide statussen een zijbalk hebben, zien de overgangs-pseudo-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 echter alleen op de nieuwe pagina staat, zal het pseudo-element ::view-transition-old(sidebar) er niet zijn. Omdat er geen 'oude' afbeelding voor de zijbalk is, zal het afbeeldingenpaar alleen een ::view-transition-new(sidebar) hebben. Evenzo, als de zijbalk alleen op de oude pagina staat, zal het afbeeldingenpaar alleen een ::view-transition-old(sidebar) hebben.

In de vorige demo verloopt de overgang van de zijbalk anders, afhankelijk van of deze in beide toestanden verschijnt, verdwijnt of aanwezig is. De zijbalk verschijnt door van rechts te schuiven en in te faden, verdwijnt door naar rechts te schuiven en uit te faden, en blijft op zijn plaats wanneer deze in beide toestanden aanwezig is.

Om specifieke in- en uitgangsovergangen te maken, kunt u de pseudo-klasse :only-child gebruiken om de oude of nieuwe pseudo-elementen te targeten wanneer dit het enige onderliggende element 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 toestanden aanwezig is, aangezien de standaardinstelling perfect is.

Async DOM-updates en wachten op inhoud

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

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

De overgang wordt pas gestart als de belofte is vervuld. Gedurende deze tijd is de pagina bevroren, dus vertragingen moeten hier tot een minimum worden beperkt. Netwerkophalingen moeten met name worden uitgevoerd vóór het aanroepen van .startViewTransition() , terwijl de pagina nog volledig interactief is, in plaats van ze uit te voeren als onderdeel van de .startViewTransition() callback.

Als u besluit te wachten tot afbeeldingen of lettertypen klaar 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)]);
});

Soms is het echter beter om de vertraging helemaal te vermijden en de content te gebruiken die u al hebt.


Haal het maximale uit de content die je al hebt

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

De miniatuur gaat over in een grotere afbeelding. Probeer de demosite .

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

Eén manier om dit aan te pakken is door te wachten tot de volledige afbeelding geladen is voordat de overgang begint. Idealiter zou dit gebeuren voordat .startViewTransition() wordt aangeroepen, zodat de pagina interactief blijft en een spinner kan worden getoond om de gebruiker te laten weten dat er iets geladen wordt. 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;
}

De miniatuur vervaagt nu niet meer, maar blijft gewoon onder de volledige afbeelding staan. Dit betekent dat, zelfs als de nieuwe weergave nog niet geladen is, de miniatuur gedurende de hele overgang zichtbaar blijft. Dit betekent dat de overgang direct kan starten en de volledige afbeelding op zijn eigen tempo 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.

Wijzigingen in beeldverhouding verwerken

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

Eén element dat overgaat in een ander, met een verandering in beeldverhouding. Minimale demo . Bron .

In de standaardovergang animeert de groep van de voor- naar de na-grootte. De oude en nieuwe weergaven hebben 100% breedte van de groep en automatische hoogte, wat betekent dat ze hun beeldverhouding behouden, ongeacht de grootte van de groep.

Dit is een goede standaardinstelling, maar in dit geval is het 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 staan ​​als de breedte toeneemt, maar dat de volledige afbeelding wordt 'ontsneden' bij de overgang van 1:1 naar 16:9.

Voor meer gedetailleerde informatie, zie Weergave-overgangen: Omgaan met wijzigingen in de beeldverhouding


Gebruik mediaquery's om overgangen voor verschillende apparaatstatussen te wijzigen

Het kan zijn dat u verschillende overgangen wilt gebruiken op mobiele apparaten en desktops, zoals in dit voorbeeld waarbij op mobiele apparaten een volledige slide vanaf de zijkant wordt uitgevoerd, maar op desktops een subtielere slide:

Eén element dat overgaat in een ander. Minimale demo . Bron .

Dit kan worden bereikt met behulp van reguliere media queries:

/* 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 welke elementen u een view-transition-name toewijst, afhankelijk van de overeenkomende media query's.


Reageer op de voorkeur 'verminderde beweging'

Gebruikers kunnen via hun besturingssysteem aangeven dat zij de voorkeur geven aan beperkte beweging. Deze voorkeur wordt vervolgens weergegeven in CSS .

U kunt ervoor kiezen om 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 'beperkte beweging' betekent echter niet dat de gebruiker geen beweging wil. In plaats van het voorgaande fragment zou je kunnen kiezen voor een subtielere animatie, die echter nog steeds de relatie tussen elementen en de datastroom weergeeft.


Meerdere weergave-overgangsstijlen verwerken met weergave-overgangstypen

Browser Support

  • Chroom: 125.
  • Rand: 125.
  • Firefox: niet ondersteund.
  • Safari: 18.

Soms heeft een overgang van de ene naar de andere weergave een specifiek aangepaste overgang nodig. Bijvoorbeeld, wanneer u naar de volgende of vorige pagina in een paginavolgorde gaat, kunt u de inhoud in een andere richting schuiven, afhankelijk van of u naar een hogere of lagere pagina in de reeks gaat.

Opname van de paginatiedemo . Deze gebruikt verschillende overgangen, afhankelijk van de pagina die u bezoekt.

Hiervoor kunt u weergaveovergangstypen gebruiken, waarmee u een of meer typen aan een actieve weergaveovergang kunt toewijzen. Gebruik bijvoorbeeld het type forwards bij de overgang naar een hogere pagina in een paginavolgorde en het type ' backwards bij de overgang naar een lagere pagina. Deze typen zijn alleen actief bij het vastleggen of uitvoeren van een overgang, en elk type kan via CSS worden aangepast voor verschillende animaties.

Om typen te gebruiken in een overgang tussen weergaven van hetzelfde document, geeft u types door aan de startViewTransition methode. Om dit mogelijk te maken, accepteert document.startViewTransition ook een object: update is de callbackfunctie die de DOM bijwerkt en types is een array met de typen.

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});

Gebruik de selector :active-view-transition-type() om op deze typen te reageren. Geef het gewenste type door aan de selector. Zo kunt u de stijlen van meerdere weergaveovergangen van elkaar gescheiden houden, zonder dat de declaraties van de ene stijl de declaraties van de andere beïnvloeden.

Omdat typen alleen van toepassing zijn bij het vastleggen of uitvoeren van de overgang, kunt u de selector gebruiken om een view-transition-name op een element alleen in of uit te schakelen voor de weergaveovergang met dat type.

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

In de volgende demo van paginering schuift de pagina-inhoud vooruit of achteruit op basis van het paginanummer waarnaar u navigeert. De typen worden bepaald door te klikken en worden doorgegeven aan document.startViewTransition .

Als u een actieve weergave-overgang wilt targeten, ongeacht het type, kunt u in plaats daarvan de pseudo-klasseselector :active-view-transition gebruiken.

html:active-view-transition {
    
}

Meerdere weergave-overgangsstijlen verwerken met een klassenaam op de weergave-overgangsroot

Soms heeft een overgang van het ene naar het andere type weergave een specifiek aangepaste overgang nodig. Of een 'terug'-navigatie moet anders zijn dan een 'vooruit'-navigatie.

Verschillende overgangen bij het 'teruggaan'. Minimale demo . Bron .

Vóór overgangstypen werden deze gevallen afgehandeld door tijdelijk een klassenaam in te stellen op de overgangsroot. Bij het aanroepen van document.startViewTransition is deze overgangsroot het <html> -element, toegankelijk via 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');
}

Om de klassen te verwijderen nadat de overgang is voltooid, gebruikt dit voorbeeld transition.finished , een promise die wordt opgelost zodra de overgang 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 media queries kan de aanwezigheid van deze klassen ook worden gebruikt om te wijzigen welke elementen een view-transition-name krijgen.


Overgangen uitvoeren zonder andere animaties te bevriezen

Bekijk deze demo van een video waarin een overgangspositie wordt getoond:

Video-overgang. Minimale demo . Bron .

Zag je er iets vreemds aan? Maak je geen zorgen als dat niet zo was. Hier zie je het, maar dan vertraagd:

Video-overgang, langzamer. Minimale demo . Bron .

Tijdens de overgang lijkt de video te bevriezen, waarna de afgespeelde versie van de video geleidelijk in beeld komt. Dit komt doordat ::view-transition-old(video) een screenshot is van de oude weergave, terwijl ::view-transition-new(video) een live -afbeelding is van de nieuwe weergave.

Je kunt dit oplossen, maar vraag jezelf eerst af of het de moeite waard is. Als je het 'probleem' niet zag toen de overgang op normale snelheid werd afgespeeld, zou ik het niet veranderen.

Als je het echt wilt oplossen, laat dan ::view-transition-old(video) niet zien; schakel direct over naar ::view-transition-new(video) . Je 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 alles!

Video-overgang, langzamer. Minimale demo . Bron .

Nu wordt de video gedurende de hele overgang afgespeeld.


Integratie met de Navigatie API (en andere frameworks)

Weergaveovergangen worden zo gespecificeerd dat ze kunnen worden geïntegreerd met andere frameworks of bibliotheken. Als uw single-page application (SPA) bijvoorbeeld een router gebruikt, kunt u het updatemechanisme van de router aanpassen om de inhoud bij te werken met behulp van een weergaveovergang.

In het volgende codefragment, afkomstig uit deze pagineringsdemo, wordt de onderscheppingshandler van de Navigatie-API aangepast om document.startViewTransition aan te roepen wanneer weergaveovergangen worden ondersteund.

navigation.addEventListener("navigate", (e) => {
    // Don't intercept if not needed
    if (shouldNotIntercept(e)) return;

    // Intercept the navigation
    e.intercept({
        handler: async () => {
            // Fetch the new content
            const newContent = await fetchNewContent(e.destination.url, {
                signal: e.signal,
            });

            // The UA does not support View Transitions, or the UA
            // already provided a Visual Transition by itself (e.g. swipe back).
            // In either case, update the DOM directly
            if (!document.startViewTransition || e.hasUAVisualTransition) {
                setContent(newContent);
                return;
            }

            // Update the content using a View Transition
            const t = document.startViewTransition(() => {
                setContent(newContent);
            });
        }
    });
});

Sommige, maar niet alle, browsers bieden een eigen overgang wanneer de gebruiker een veegbeweging maakt om te navigeren. In dat geval moet u uw eigen weergaveovergang niet activeren, omdat dit zou leiden tot een slechte of verwarrende gebruikerservaring. De gebruiker ziet dan twee overgangen – één van de browser en één van u – die achter elkaar worden weergegeven.

Daarom is het raadzaam om te voorkomen dat een weergaveovergang start wanneer de browser een eigen visuele overgang heeft voorzien. Controleer hiervoor de waarde van de eigenschap hasUAVisualTransition van de NavigateEvent instantie. Deze eigenschap wordt ingesteld op true wanneer de browser een visuele overgang heeft voorzien. Deze hasUIVisualTransition eigenschap bestaat ook in PopStateEvent instanties.

In het vorige fragment wordt bij de controle die bepaalt of de weergaveovergang moet worden uitgevoerd, rekening gehouden met deze eigenschap. Als er geen ondersteuning is voor weergaveovergangen binnen hetzelfde document of als de browser al een eigen overgang heeft, wordt de weergaveovergang overgeslagen.

if (!document.startViewTransition || e.hasUAVisualTransition) {
  setContent(newContent);
  return;
}

In de volgende opname veegt de gebruiker om terug te navigeren naar de vorige pagina. De opname links bevat geen controle op de hasUAVisualTransition vlag. De opname rechts bevat wel een controle, waardoor de handmatige weergaveovergang wordt overgeslagen omdat de browser een visuele overgang heeft.

Vergelijking van dezelfde site zonder (links) en breedte (rechts) een controle op hasUAVisualTransition

Animeren met JavaScript

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

Cirkelovergang. Minimale demo . Bron .

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

  • De animatie start vanaf de kliklocatie.
  • De animatie eindigt met een cirkel met een straal tot aan de verste hoek. Hopelijk wordt dit in de toekomst mogelijk met CSS.

Gelukkig kunt u 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 promise die wordt opgelost zodra de pseudo-elementen van de transitie succesvol zijn aangemaakt. Andere eigenschappen van dit object worden behandeld in de API-referentie .


Transities als verbetering

De View Transition API is ontworpen om een ​​DOM-wijziging te 'verpakken' en er een transitie voor te creëren. De transitie moet echter als een verbetering worden beschouwd, dat wil zeggen dat uw app niet in een 'fout'-status mag terechtkomen als de DOM-wijziging slaagt, maar de transitie mislukt. Idealiter zou de transitie niet moeten mislukken, maar als dat wel gebeurt, mag dit de rest van de gebruikerservaring niet verstoren.

Om overgangen als een verbetering te behandelen, moet u ervoor zorgen dat u overgangsbeloftes niet op een manier gebruikt waardoor uw app in de war raakt 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() wordt afgewezen als de overgang de status ' ready niet kan bereiken. Dit betekent echter niet dat de weergave niet is overgeschakeld. De DOM is mogelijk wel succesvol bijgewerkt, maar er waren dubbele view-transition-name 's', waardoor de overgang is 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 deze af te wijzen als deze mislukt. switchView wijst niet langer af als de transitie mislukt, maar lost dit op wanneer de DOM-update is voltooid en wijst dit af als deze mislukt.

Als u wilt dat switchView wordt omgezet zodra de nieuwe weergave is 'gesetteld', dat wil zeggen wanneer een geanimeerde overgang is voltooid of naar het einde is gesprongen, vervangt u transition.updateCallbackDone door transition.finished .


Geen polyfill, maar...

Dit is geen eenvoudige functie om te polyfillen. Deze helperfunctie maakt het echter een stuk eenvoudiger in browsers die geen weergaveovergangen ondersteunen:

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

En het kan als volgt gebruikt worden:

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

  const transition = transitionHelper({
    update() {
      updateTheDOMSomehow(data);
    },
    types,
  });

  // …
}

In browsers die geen weergave-overgangen ondersteunen, wordt updateDOM nog steeds aangeroepen, maar is er geen sprake van een geanimeerde overgang.

U kunt ook een aantal classNames opgeven die u tijdens de overgang aan <html> kunt toevoegen. Zo kunt u de overgang eenvoudiger wijzigen, afhankelijk van het type navigatie .

Je kunt ook true aan skipTransition doorgeven als je geen animatie wilt, zelfs in browsers die weergaveovergangen ondersteunen. Dit is handig als je site een gebruikersvoorkeur heeft om overgangen uit te schakelen.


Werken met frameworks

Als je werkt met een bibliotheek of framework dat DOM-wijzigingen abstraheert, is het lastig om te weten wanneer de DOM-wijziging voltooid is. Hier zijn een aantal voorbeelden, met behulp van de bovenstaande helper , in verschillende frameworks.

  • React — de sleutel hier is flushSync , dat een reeks statuswijzigingen synchroon toepast. Ja, er staat een grote waarschuwing op het gebruik van die API, maar Dan Abramov verzekert me dat het in dit geval gepast is. Zoals gebruikelijk bij React en async-code, zorg er bij het gebruik van de verschillende promises die startViewTransition retourneert voor dat je code met de juiste status wordt uitgevoerd.
  • Vue.js — de sleutel hier is nextTick , die wordt uitgevoerd zodra de DOM is bijgewerkt.
  • Svelte —erg vergelijkbaar met Vue, maar de methode om te wachten op de volgende wijziging is tick .
  • Lit — de sleutel hierbij is de this.updateComplete -belofte binnen componenten, die wordt vervuld zodra de DOM is bijgewerkt.
  • Angular — de sleutel hier is applicationRef.tick , die in behandeling zijnde DOM-wijzigingen verwijdert. Vanaf Angular versie 17 kun je withViewTransitions gebruiken, dat standaard bij @angular/router wordt geleverd .

API-referentie

const viewTransition = document.startViewTransition(update)

Start een nieuwe ViewTransition .

update is een functie die wordt aangeroepen zodra de huidige status van het document is vastgelegd.

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

const viewTransition = document.startViewTransition({ update, types })

Start een nieuwe ViewTransition met de opgegeven typen

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

types worden de actieve typen voor de overgang ingesteld bij het vastleggen of uitvoeren van de overgang. Deze is aanvankelijk leeg. Zie viewTransition.types verderop voor meer informatie.

Instantieleden van ViewTransition :

viewTransition.updateCallbackDone

Een belofte die wordt uitgevoerd als de belofte die door updateCallback wordt geretourneerd, wordt uitgevoerd, of wordt afgewezen als deze wordt afgewezen.

De View Transition API verpakt een DOM-wijziging en creëert een overgang. Soms maakt het je echter niet uit of de overgangsanimatie slaagt of mislukt, je wilt gewoon weten of en wanneer de DOM-wijziging plaatsvindt. updateCallbackDone is hiervoor geschikt.

viewTransition.ready

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

De melding wordt afgewezen als de overgang 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 nagekomen zodra de eindtoestand volledig zichtbaar en interactief is voor de gebruiker.

Er vindt alleen een afwijzing plaats als updateCallback een afgewezen belofte retourneert, aangezien dit aangeeft dat de eindstatus niet is gemaakt.

Anders, als een overgang niet start of wordt overgeslagen tijdens de overgang, wordt de eindtoestand toch bereikt en is finished vervuld.

viewTransition.types

Een Set achtig object dat de typen van de actieve weergaveovergang bevat. Om de items te manipuleren, gebruikt u de instantiemethoden clear() , add() en delete() .

Om te reageren op een specifiek type in CSS, gebruikt u de pseudo-klasseselector :active-view-transition-type(type) op de overgangsroot.

Typen worden automatisch opgeruimd wanneer de weergaveovergang is voltooid.

viewTransition.skipTransition()

Sla het animatiegedeelte van de overgang over.

Hiermee wordt 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.

width en height tussen de 'voor'- en 'na'-toestanden.

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

::view-transition-image-pair

Absoluut in staat om de groep te vullen.

Heeft isolation: isolate om het effect van de mix-blend-mode op de oude en nieuwe weergaven te beperken.

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

Absoluut gepositioneerd linksboven op de wrapper.

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

Heeft mix-blend-mode: plus-lighter voor een echte cross-fade.

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


Feedback

Feedback van ontwikkelaars wordt altijd gewaardeerd. Om dit te doen, kunt u een probleem indienen bij de CSS Working Group op GitHub met suggesties en vragen. Voeg vóór uw probleem het voorvoegsel [css-view-transitions] toe.

Mocht u een bug tegenkomen, meld dit dan als Chromium-bug .