Weergaveovergangen van hetzelfde document voor toepassingen met één pagina

Gepubliceerd: 17 augustus 2021, Laatst bijgewerkt: 25 september 2024

Wanneer een weergaveovergang op één document wordt uitgevoerd, wordt dit een weergaveovergang voor hetzelfde document genoemd. Dit is doorgaans het geval bij toepassingen met één pagina (SPA's) waarbij JavaScript wordt gebruikt om de DOM bij te werken. Overgangen naar weergaven van hetzelfde document worden vanaf Chrome 111 ondersteund in Chrome.

Om een ​​weergaveovergang voor hetzelfde document 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 de browser wordt aangeroepen, maakt deze automatisch momentopnamen van alle elementen waarvoor een CSS-eigenschap view-transition-name is gedeclareerd.

Vervolgens voert het de doorgegeven callback uit die de DOM bijwerkt, waarna er momentopnamen worden gemaakt van de nieuwe status.

Deze snapshots worden vervolgens gerangschikt in een boom van pseudo-elementen en geanimeerd met behulp van de kracht van CSS-animaties. Paren van snapshots uit de oude en nieuwe staat gaan soepel over van hun oude positie en grootte naar hun nieuwe locatie, terwijl hun inhoud vervaagt. Als je wilt, kun je CSS gebruiken om de animaties aan te passen.


De standaardovergang: Cross-fade

De standaard weergaveovergang 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 zoals jij wilt. U kunt bijvoorbeeld elementen toevoegen of verwijderen, klassenamen wijzigen of stijlen wijzigen.

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 eerst moet je begrijpen hoe deze fundamentele cross-fade werkte.


Hoe deze transities 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. Hierbij hoort ook het maken van een momentopname.

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

Zodra de nieuwe 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.

Pas de overgang aan

Alle pseudo-elementen van de weergaveovergang kunnen worden getarget met CSS, en aangezien de animaties worden gedefinieerd met behulp van CSS, kunt u ze wijzigen 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. 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 hier is het resultaat:

Gedeelde asovergang. Minimale demo . Bron .

Overgang van meerdere elementen

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 koptekst 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 (met behulp van 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 je kunt de tekst ook 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, in plaats van dezelfde grootte op beide pagina's.

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.


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

Browserondersteuning

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

Stel dat u een weergaveovergang heeft 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 zich op elke individuele kaart richt.

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? Dat zijn 20 selectors die u moet schrijven. Een nieuw element toevoegen? Dan moet je ook de selector laten groeien die de animatiestijlen toepast. Niet bepaald schaalbaar.

De view-transition-class kan worden gebruikt in de view-transition pseudo-elementen 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 voorbeeld van de kaart maakt gebruik van het vorige CSS-fragment. Op alle kaarten, inclusief nieuw toegevoegde kaarten, wordt dezelfde timing toegepast met één selector: html::view-transition-group(.card) .

Opname van de Cards-demo . Met behulp van view-transition-class past het dezelfde animation-timing-function toe op alle kaarten behalve de toegevoegde of verwijderde.

Debug 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 .

Foutopsporing in weergaveovergangen met Chrome DevTools.

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 voor deze overgang is iets ingewikkelder dan het voorgaande voorbeeld, 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 sluiten. 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 vorige demo verandert de zijbalk anders, 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 de oude of nieuwe pseudo-elementen 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.


Maak optimaal gebruik van de inhoud die u al heeft

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

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

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.

Wijzigingen in de beeldverhouding verwerken

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 bijgesneden wanneer deze overgaat van 1:1 naar 16:9.

Voor meer gedetailleerde informatie, ga naar Overgangen bekijken: Wijzigingen in de beeldverhouding verwerken


Gebruik mediaquery's om overgangen voor verschillende apparaatstatussen te wijzigen

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.


Reageer op de voorkeur 'verminderde beweging'

Gebruikers kunnen via hun besturingssysteem aangeven dat ze de voorkeur geven aan verminderde beweging, en die voorkeur wordt weergegeven in CSS .

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 voorgaande fragment zou je een subtielere animatie kunnen kiezen, maar wel één die nog steeds de relatie tussen de elementen en de gegevensstroom tot uitdrukking brengt.


Behandel meerdere weergave-overgangsstijlen met weergave-overgangstypen

Browserondersteuning

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

Soms moet een transitie van de ene visie naar de andere een specifiek op maat gemaakte transitie kennen. Als u bijvoorbeeld naar de volgende of vorige pagina in een pagineringsreeks gaat, wilt u de inhoud mogelijk in een andere richting schuiven, afhankelijk van of u naar een hogere of een lagere pagina in de reeks gaat.

Opname van de Paginatie-demo . Er worden verschillende overgangen gebruikt, afhankelijk van naar welke pagina u gaat.

Hiervoor kunt u gebruik maken van weergaveovergangstypen, waarmee u één of meerdere typen aan een actieve weergaveovergang kunt toewijzen. Als u bijvoorbeeld naar een hogere pagina in een pagineringsreeks gaat, gebruikt u het forwards type en als u naar een lagere pagina gaat, gebruikt u het backwards type. Deze typen zijn alleen actief bij het vastleggen of uitvoeren van een overgang, en elk type kan via CSS worden aangepast om verschillende animaties te gebruiken.

Als u typen wilt gebruiken in een weergaveovergang met hetzelfde document, geeft u types door aan de methode startViewTransition . Om dit mogelijk te maken accepteert document.startViewTransition ook een object: update is de callback-functie die de DOM bijwerkt, en types is een array met de typen.

const direction = determineBackwardsOrForwards();

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

Om op deze typen te reageren, gebruikt u de :active-view-transition-type() selector. Geef het type dat u wilt targeten, door in de selector. Hierdoor kunt u de stijlen van meerdere weergaveovergangen van elkaar gescheiden houden, zonder dat de declaraties van de een interfereren met de declaraties van de ander.

Omdat typen alleen van toepassing zijn bij het vastleggen of uitvoeren van de overgang, kunt u de selector gebruiken om een view-transition-name alleen voor een element in te stellen (of uit te schakelen) voor de weergave-overgang 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 pagineringsdemo schuift de pagina-inhoud vooruit of achteruit op basis van het paginanummer waarnaar u navigeert. De typen worden bepaald door te klikken, waarna ze worden doorgegeven aan document.startViewTransition .

Om elke actieve weergaveovergang te targeten, ongeacht het type, kunt u in plaats daarvan de :active-view-transition pseudo-klasse selector gebruiken.

html:active-view-transition {
    
}

Behandel meerdere weergave-overgangsstijlen met een klassenaam in de weergave-overgangswortel

Soms moet een transitie van het ene type visie naar het andere een specifiek op maat gemaakte transitie kennen. Of een 'terug'-navigatie moet anders zijn dan een 'vooruit'-navigatie.

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

Vóór overgangstypen was de manier om deze gevallen af ​​te handelen het tijdelijk instellen van een klassenaam op de overgangswortel. Bij het aanroepen van document.startViewTransition is deze transitieroot 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 transitie is voltooid, gebruikt dit voorbeeld transition.finished , een belofte die wordt opgelost 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.


Voer overgangen uit 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.


Integratie met de Navigatie-API (en andere frameworks)

Bekijkovergangen zijn zo gespecificeerd dat ze kunnen worden geïntegreerd met andere raamwerken of bibliotheken. Als uw single-page applicatie (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 uit deze pagineringsdemo is 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 hun eigen overgang wanneer de gebruiker een veeggebaar maakt om te navigeren. In dat geval mag u niet uw eigen weergaveovergang activeren, omdat dit zou leiden tot een slechte of verwarrende gebruikerservaring. De gebruiker ziet twee overgangen (de ene door de browser en de andere door u) achter elkaar worden uitgevoerd.

Daarom wordt aanbevolen om te voorkomen dat een weergaveovergang start wanneer de browser voor zijn eigen visuele overgang heeft gezorgd. Om dit te bereiken, controleert u de waarde van de eigenschap hasUAVisualTransition van de NavigateEvent instantie. De eigenschap wordt ingesteld op true wanneer de browser voor een visuele overgang heeft gezorgd. Deze eigenschap hasUIVisualTransition 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 van hetzelfde document of als de browser al een eigen overgang heeft geleverd, 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 aan de linkerkant bevat geen controle op de vlag hasUAVisualTransition . De opname rechts bevat wel de controle, waarbij de handmatige weergaveovergang wordt overgeslagen omdat de browser voor een visuele overgang zorgde.

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:

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

Dit is geen gemakkelijke functie voor polyfill. Deze helperfunctie maakt het echter veel 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 worden gebruikt:

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

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

  // …
}

In browsers die weergaveovergangen 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 als u geen animatie wilt, zelfs in browsers die weergaveovergangen 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(update)

Start een nieuwe ViewTransition .

update is een functie die 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.

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 stelt de actieve typen in voor de overgang bij het vastleggen of uitvoeren van de overgang. Het is aanvankelijk leeg. Zie viewTransition.types verderop voor meer informatie.

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 je echter niets uit of de transitie-animatie slaagt of mislukt, maar wil je gewoon weten of en wanneer de DOM-verandering 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 tijdens de transitie wordt overgeslagen, wordt de eindtoestand nog steeds bereikt, dus finished is vervuld.

viewTransition.types

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

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

Typen worden automatisch opgeschoond wanneer de weergaveovergang is voltooid.

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 mix-blend-mode op de oude en nieuwe weergaven te beperken.

::view-transition-new ::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 wordt altijd op prijs gesteld. Om dit te doen, dient u een probleem in bij de CSS Working Group op GitHub met suggesties en vragen. Geef uw probleem een ​​voorvoegsel met [css-view-transitions] .

Mocht je een bug tegenkomen, dien dan een Chromium-bug in .