Płynne i proste przechodzenie dzięki interfejsowi View Transitions API

Jake Archibald
Jake Archibald

Obsługa przeglądarek

  • 111
  • 111
  • x
  • x

Źródło

Interfejs View Transition API ułatwia zmianę modelu DOM w jednym kroku i przy tym tworzy animowane przejście między dwoma stanami. Jest dostępna w Chrome 111 i nowszych wersjach.

Przejścia utworzone za pomocą interfejsu View Transition API. Wypróbuj stronę demonstracyjną – wymaga Chrome 111 lub nowszej wersji.

Dlaczego potrzebujemy tej funkcji?

Przejścia między stronami nie tylko wyglądają dobrze, ale także pokazują kierunek przepływu i jednoznacznie wskazują, które elementy są powiązane między stronami. Mogą one mieć miejsce nawet podczas pobierania danych, co oznacza szybsze postrzeganie skuteczności.

Jednak w internecie mamy już narzędzia do animacji, takie jak przejścia CSS, animacje CSS i Web Animation API. Dlaczego potrzebujemy nowego narzędzia do przenoszenia elementów?

Prawda jest taka, że przejście na różne stany jest trudne, nawet przy pomocy narzędzi, które już mamy.

Nawet proste przenikanie polega na obecności obu stanów w tym samym czasie. Wiąże się to z problemami z użytecznością, na przykład obsługą dodatkowych interakcji w elemencie wychodzącym. Ponadto w przypadku użytkowników urządzeń wspomagających występuje okres, w którym w modelu DOM występuje jednocześnie stan „przed” i „po”, a elementy mogą swobodnie poruszać się po drzewie, ale może to łatwo doprowadzić do utraty pozycji i punktu widzenia.

Obsługa zmian stanu jest szczególnie trudna, jeśli oba stany różnią się położeniem przewijania. Jeśli element jest przenoszony z jednego kontenera do drugiego, mogą wystąpić problemy z overflow: hidden i innymi formami przycinania. Oznacza to, że aby uzyskać odpowiedni efekt, trzeba zmienić strukturę CSS.

To nie niemożliwe, to po prostu naprawdę trudne.

Przejścia widoku ułatwiają wprowadzanie zmian DOM bez nakładania się między stanami, ale przy użyciu widoków zrzutów można utworzyć animację przejścia między stanami.

Chociaż obecna implementacja dotyczy aplikacji jednostronicowych, ta funkcja zostanie rozszerzona, aby umożliwić przejście między pełnym wczytaniem strony, co obecnie jest niemożliwe.

Stan standaryzacji

Funkcja jest opracowywana w grupie roboczej W3C CSS Workspace jako wersja robocza.

Gdy uznamy, że interfejs API jest zgodny z oczekiwaniami, rozpoczniemy procesy i kontrole, które pozwolą nam wprowadzić tę funkcję do stabilnej wersji systemu.

Opinie deweloperów są dla nas bardzo ważne, dlatego zgłaszaj problemy na GitHubie, dodając sugestie i pytania.

Najprostsze przejście: przenikanie

Domyślne przejście w widoku jest stopniowe, więc stanowi dobre wprowadzenie do interfejsu 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));
}

Gdzie updateTheDOMSomehow zmienia DOM na nowy. Możesz to robić w dowolny sposób: dodawać/usuwać elementy, zmieniać nazwy klas, zmieniać style – nie ma znaczenia.

I w ten sposób strony przenikają:

Domyślna opcja przenikania. Minimalna wersja demonstracyjna. Źródło.

Przeniknięcie nie jest aż tak imponujące. Na szczęście przejścia można dostosować, ale zanim to zrobimy, musimy zrozumieć, jak działa ta podstawowa metoda stopniowego przejścia.

Jak przebiega przenoszenie kont

Pobierając przykładowy kod z podanego wyżej kodu:

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

Po wywołaniu interfejsu .startViewTransition() interfejs API przechwytuje bieżący stan strony. Dotyczy to też robienia zrzutu ekranu.

Po jego zakończeniu wywoływane jest wywołanie zwrotne przekazane do .startViewTransition(). To właśnie tutaj zmienia się DOM. Następnie interfejs API przechwytuje nowy stan strony.

Po przechwyceniu stanu interfejs API tworzy pseudoelementowe drzewo w ten sposób:

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

Element ::view-transition umieszcza się w nakładce, nad całą resztą strony. Jest to przydatne, gdy chcesz ustawić kolor tła przejścia.

::view-transition-old(root) to zrzut ekranu starego widoku, a ::view-transition-new(root) przedstawia nowy widok w czasie rzeczywistym. Obie są renderowane jako „zastępowana treść” CSS (np. <img>).

Stary widok zmienia kolor od opacity: 1 do opacity: 0, a nowy z opacity: 0 do opacity: 1 zmienia się w przenikanie.

Cała animacja jest wykonywana przy użyciu animacji CSS, więc można je dostosować za pomocą CSS.

Proste dostosowanie

Wszystkie te pseudoelementy można kierować za pomocą CSS, a ponieważ animacje są zdefiniowane za pomocą CSS, możesz je modyfikować, korzystając z istniejących właściwości animacji CSS. Na przykład:

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

Po tej zmianie przyciemnienie jest bardzo powolne:

Długie przenikanie. Minimalna wersja demonstracyjna. Źródło.

To nadal nie jest imponujące. Zamiast tego wdrożymy przejście wspólnej osi w 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;
}

Oto wynik:

Przejście ze wspólnej osi. Minimalna wersja demonstracyjna. Źródło.

Przenoszenie wielu elementów

W poprzedniej wersji demonstracyjnej przejście do wspólnej osi odbywa się na całej stronie. To rozwiązanie sprawdza się w przypadku większości strony, ale nie za dobrze w nagłówku, ponieważ wysuwa się, aby z powrotem się wysunąć.

Aby tego uniknąć, możesz wyodrębnić nagłówek z pozostałej części strony, aby można go było osobno animować. Aby to zrobić, przypisujemy elementowi view-transition-name.

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

Wartość view-transition-name może być dowolna (oprócz none, co oznacza, że nie ma nazwy przejścia). Służy do unikalnego określenia elementu przejścia.

Efekt:

Przejście współdzielonej osi ze stałym nagłówkiem. Minimalna wersja demonstracyjna. Źródło.

Teraz nagłówek pozostaje na swoim miejscu i zanikanie.

Ta deklaracja CSS spowodowała zmianę pseudoelementu:

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

Istnieją teraz 2 grupy przejścia. Jedna na nagłówek, a druga – do pozostałych. Mogą one być kierowane niezależnie za pomocą CSS i mogą mieć różne przejścia. W tym przypadku main-header zostało jednak z domyślnym przejściem, które jest przenikaniem.

OK, domyślne przejście to nie tylko przenikanie, ale też ::view-transition-group.

  • Położenie i przekształcenie (za pomocą transform)
  • Szerokość
  • Wzrost

Do tej pory nie miało to znaczenia, ponieważ nagłówek ma ten sam rozmiar i położenie po obu stronach zmiany DOM. Możemy też wyodrębnić tekst z nagłówka:

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

W użyciu jest używany format fit-content, dzięki któremu element ma rozmiar tekstu, a nie rozciąganie go do końca. Bez niej strzałka wstecz zmniejsza rozmiar elementu tekstowego nagłówka, a chcemy, aby na obu stronach był ten sam.

Teraz zajmiemy się 3 częściami:

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

Wróćmy jednak do ustawień domyślnych:

Przesuwny tekst nagłówka. Minimalna wersja demonstracyjna. Źródło.

Teraz tekst nagłówka przesuwa się po ekranie, aby zrobić miejsce na przycisk Wstecz.

Debugowanie przejścia

Przejścia widoku są tworzone na podstawie animacji CSS, dlatego panel Animacje w Narzędziach deweloperskich w Chrome doskonale nadaje się do debugowania przejść.

Panel Animacje pozwala wstrzymać następną animację, a następnie przewijać animację do przodu i do tyłu. Podczas tego procesu pseudoelementy przejścia znajdują się w panelu Elementy.

Debugowanie przejść z widoku danych za pomocą narzędzi deweloperskich w Chrome.

Przenoszone elementy nie muszą być tym samym elementem DOM

Do tej pory tworzyliśmy osobne elementy przejścia dla nagłówka i tekstu w nagłówku za pomocą view-transition-name. Są to koncepcyjnie te same elementy przed zmianą DOM i po niej, ale możesz tworzyć przejścia tam, gdzie nie jest to konieczne.

Na przykład elementowi umieszczonemu na stronie głównej można nadać view-transition-name:

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

Następnie po kliknięciu miniatury można otrzymać taki sam atrybut view-transition-name na czas przejścia:

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

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

A wynik:

Jeden element przechodzi do innego. Minimalna wersja demonstracyjna. Źródło.

Miniatura przełączy się na obraz główny. Chociaż są one koncepcyjnie (i dosłownie) różnymi elementami, interfejs API przejścia traktuje je tak samo, ponieważ mają wspólny element view-transition-name.

Rzeczywisty kod jest nieco bardziej skomplikowany niż ten prosty przykład powyżej, ponieważ obejmuje również przejście z powrotem na stronę miniatury. Pełną implementację znajdziesz w źródle.

Niestandardowe przejścia wejścia i wyjścia

Przeanalizuj ten przykład:

Wprowadzanie i opuszczanie paska bocznego. Minimalna wersja demonstracyjna. Źródło.

Pasek boczny jest częścią przejścia:

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

Jednak w przeciwieństwie do nagłówka w poprzednim przykładzie pasek boczny nie pojawia się na wszystkich stronach. Jeśli oba stany mają pasek boczny, pseudoelementy przejścia wyglądają tak:

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

Jeśli jednak pasek boczny znajduje się tylko na nowej stronie, pseudoelementu ::view-transition-old(sidebar) nie będzie. Nie ma „starego” obrazu na pasku bocznym, więc para obrazów będzie zawierać tylko element ::view-transition-new(sidebar). Podobnie jeśli pasek boczny znajduje się tylko na starej stronie, para obrazów będzie zawierać tylko atrybut ::view-transition-old(sidebar).

W wersji demonstracyjnej powyżej pasek boczny przechodzi w różny sposób w zależności od tego, czy otwiera się, zamyka czy jest obecne w obu stanach. Pojawia się, przesuwając go z prawej strony i wnikając, a potem znika, przesuwając się w prawo i ściemniając. Pozostawia na miejscu, jeśli występuje w obu stanach.

Aby utworzyć określone przejścia wejścia i wyjścia, możesz użyć pseudoklasy :only-child, aby ustawić kierowanie na stary/nowy pseudoelement, gdy jest to jedyne podrzędne przejście w parze obrazów:

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

W tym przypadku nie ma konkretnego przejścia, gdy pasek boczny występuje w obu stanach, ponieważ ustawienie domyślne jest idealne.

Asynchroniczne aktualizacje DOM i oczekiwanie na treść

Wywołanie zwrotne przekazane do .startViewTransition() może zwrócić obietnicę, co umożliwia asynchroniczne aktualizacje DOM i oczekiwanie na przygotowanie ważnej treści.

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

Przenoszenie rozpocznie się dopiero wtedy, gdy obietnica się nie spełni. W tym czasie strona jest zamrożona, więc opóźnienia w tym miejscu należy ograniczyć do minimum. W szczególności pobieranie sieciowe powinno się odbywać przed wywołaniem funkcji .startViewTransition(), gdy strona jest nadal w pełni interaktywna – nie trzeba jej wykonywać w ramach wywołania zwrotnego .startViewTransition().

Jeśli zdecydujesz się poczekać, aż obrazy lub czcionki będą gotowe, ustaw wyższy limit czasu:

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

W niektórych przypadkach lepiej jednak całkowicie uniknąć opóźnienia i wykorzystać treści, które już masz.

Pełne wykorzystywanie treści, które już masz

Jeśli miniatura zmienia się w większy obraz:

Domyślnym przejściem jest przenikanie, co oznacza, że miniatura może się przenikać z jeszcze niezaładowanym pełnym obrazem.

Możesz to zrobić, czekając na wczytanie całego obrazu przed rozpoczęciem przejścia. Najlepiej zrobić to przed wywołaniem funkcji .startViewTransition(), aby strona pozostaje interaktywna i może być wyświetlany wskaźnik postępu ładowania. W tym przypadku jest jednak lepszy sposób:

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

Teraz miniatura nie znika, znajduje się tylko pod pełnym obrazem. Oznacza to, że jeśli nowy widok nie zostanie wczytany, miniatura będzie widoczna przez cały czas przejścia. Oznacza to, że przejście może rozpocząć się od razu, a pełny obraz może zostać wczytany w określonym momencie.

Nie byłoby to możliwe, jeśli nowy widok zakładał przejrzystość, ale w tym przypadku wiemy, że nie jest, więc możemy wprowadzić tę optymalizację.

Obsługa zmian formatu obrazu

Do tej pory wszystkie przejścia dotyczyły elementów o tym samym współczynniku proporcji, ale nie zawsze tak będzie. Co zrobić, jeśli miniatura ma proporcje 1:1, a główny obraz ma 16:9?

Jeden element przechodzi do innego ze zmianą proporcji. Minimalna wersja demonstracyjna. Źródło.

W domyślnym przejściu grupa animuje się z rozmiaru „przed” do „po”. Widoki stare i nowe mają 100% szerokości grupy i automatyczną wysokość, co oznacza, że zachowują proporcje niezależnie od wielkości grupy.

Jest to dobre ustawienie domyślne, ale w tym przypadku nie jest to naszym zdaniem. Przykłady:

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

Oznacza to, że miniatura pozostaje pośrodku elementu przy zwiększaniu szerokości, ale pełny obraz zostaje cofnięty po przejściu z 1:1 na 16:9.

Zmienianie przejścia w zależności od stanu urządzenia

Możesz użyć różnych przejść na komórce i komputerze, na przykład w tym przykładzie widać cały slajd z boku na komórce, a na komputerze – bardziej subtelny:

Jeden element przechodzi do innego. Minimalna wersja demonstracyjna. Źródło.

Możesz to osiągnąć, używając zwykłych zapytań o media:

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

Możesz też zmienić przypisane elementy (view-transition-name) w zależności od pasujących zapytań o multimedia.

Reakcja na ustawienie „zmniejszony ruch”

Użytkownicy mogą wskazać, że wolą zmniejszony ruch w systemie operacyjnym i to to ustawienie jest ujawniane za pomocą CSS.

Możesz uniemożliwić przeniesienie tych użytkowników:

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

Jednak ustawienie „mniejszego ruchu” nie oznacza, że użytkownik chce braku ruchu. Zamiast tego możesz wybrać bardziej subtelną animację, która nadal odzwierciedla związek między elementami i przepływem danych.

Zmienianie przejścia w zależności od typu nawigacji

Czasami przejście z jednego typu strony na inną powinno być ściśle dostosowane. Nawigacja „wstecz” powinna być inna niż „do przodu”.

Różne przejścia przy przywracaniu. Minimalna wersja demonstracyjna. Źródło.

Najlepszym sposobem obsługi takich przypadków jest ustawienie nazwy klasy w polu <html>, nazywanego też elementem dokumentu:

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

W tym przykładzie wykorzystano obiekt transition.finished, który znika po osiągnięciu stanu końcowego przeniesienia. Pozostałe właściwości tego obiektu znajdziesz w dokumentacji interfejsu API.

Teraz możesz użyć tej nazwy klasy w CSS, aby zmienić przejście:

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

Podobnie jak w przypadku zapytań o multimedia, obecność tych klas może też pozwalać na zmianę elementów, które otrzymują view-transition-name.

Przejścia bez blokowania innych animacji

Spójrz na tę prezentację pozycji przejścia wideo:

Przejście wideo. Minimalna wersja demonstracyjna. Źródło.

Czy widzisz, że coś jest z nim nie tak? Nie martw się, jeśli tak nie jest. Tutaj tempo jest spowolnione:

Przejście wideo, wolniejsze. Minimalna wersja demonstracyjna. Źródło.

W trakcie przejścia film wydaje się się zawieszać, a następnie pojawia się odtwarzana wersja filmu. Dzieje się tak, ponieważ element ::view-transition-old(video) jest zrzutem ekranu starego widoku, a element ::view-transition-new(video) to obecny obraz nowego widoku.

Możesz to naprawić, ale najpierw zastanów się, czy warto to naprawić. Jeśli nie widzisz „problemu” podczas odtwarzania przejścia z normalną szybkością, nie warto go zmieniać.

Jeśli naprawdę chcesz rozwiązać ten problem, nie pokazuj przycisku ::view-transition-old(video). Przejdź bezpośrednio do sekcji ::view-transition-new(video). Możesz to zrobić, zastępując domyślne style i animacje:

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

Gotowe!

Przejście wideo, wolniejsze. Minimalna wersja demonstracyjna. Źródło.

Teraz film odtwarza się przez cały czas trwania przejścia.

Animacja za pomocą JavaScriptu

Jak dotąd wszystkie przejścia zostały zdefiniowane za pomocą CSS, ale czasami CSS nie wystarcza:

Przejście między okręgami. Minimalna wersja demonstracyjna. Źródło.

Kilka części tego przejścia nie da się osiągnąć za pomocą samego CSS:

  • Animacja rozpoczyna się od miejsca kliknięcia.
  • Animacja kończy się okrągem, który ma promień do najdalszego rogu. Mamy jednak nadzieję, że w przyszłości będzie to możliwe w przypadku usług porównywania cen.

Na szczęście możesz tworzyć przejścia za pomocą interfejsu 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)',
      }
    );
  });
}

W tym przykładzie użyto elementu transition.ready, który znika po utworzeniu pseudoelementów przejścia. Pozostałe właściwości tego obiektu znajdziesz w dokumentacji interfejsu API.

Przejścia jako ulepszenie

Interfejs View Transition API służy do „opakowania” zmiany DOM i utworzenia dla niej przejścia. Przejście należy jednak traktować jako ulepszenie, np. jeśli zmiana DOM zakończy się powodzeniem, aplikacja nie powinna przechodzić w stan „błędu”. Przejście powinno przebiegać bez zakłóceń, ale jeśli tak, nie powinno negatywnie wpłynąć na wrażenia użytkownika.

Aby traktować przejścia jako udoskonalenie, nie używaj obiecujących przejść w sposób, który spowodowałby błąd aplikacji w przypadku niepowodzenia.

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

Problem w tym przykładzie polega na tym, że switchView() odrzuci, jeśli przejście nie może osiągnąć stanu ready, ale nie oznacza to, że nie udało się przełączyć widoku. Być może DOM został zaktualizowany, ale wystąpiły zduplikowane elementy view-transition-name, dlatego przejście zostało pominięte.

Zamiast tego:

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

W tym przykładzie użyliśmy elementu transition.updateCallbackDone do oczekiwania na aktualizację DOM i odrzucenia, jeśli się nie uda. switchView nie odrzuca już, jeśli przejście się nie powiedzie, zamknie się po zakończeniu aktualizacji DOM i odrzuca, jeśli nie uda się przenieść.

Jeśli chcesz, aby działanie switchView miało miejsce po ustabilizowaniu się nowego widoku, czyli np. w przypadku, gdy animowane przejście zostało zakończone lub pominięto do końca, zastąp transition.updateCallbackDone wartością transition.finished.

To nie jest polyfill, ale...

Nie sądzę, aby tę funkcję można było wypełnić w żaden użyteczny sposób, ale okazało się, że się pomyliliśmy.

Jednak ta funkcja pomocnicza znacznie ułatwia pracę w przeglądarkach, które nie obsługują przechodzenia między widokami:

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

Można go użyć w następujący sposób:

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

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

  // …
}

W przeglądarkach, które nie obsługują przejść w trybie wyświetlania, wywołanie updateDOM wciąż będzie wywoływane, ale przejście nie będzie animowane.

Możesz też udostępnić niektóre classNames do dodania do <html> podczas przenoszenia, aby ułatwić zmianę przejścia w zależności od typu nawigacji.

Jeśli nie potrzebujesz animacji, możesz też przekazać true do skipTransition, nawet w przeglądarkach, które obsługują przejścia w widoku widoku. Jest to przydatne, jeśli w swojej witrynie użytkownicy mają wyłączoną możliwość przenoszenia.

Praca z platformami

Jeśli używasz biblioteki lub platformy, która wyodrębnia zmiany DOM, problem polega na tym, że wiesz, kiedy zmiana DOM jest już gotowa. Oto kilka przykładów wykorzystania powyższych rozwiązań pomocnych w różnych schematach.

Dokumentacja API

const viewTransition = document.startViewTransition(updateCallback)

Rozpocznij nowe ViewTransition.

Funkcja updateCallback jest wywoływana po zarejestrowaniu bieżącego stanu dokumentu.

Gdy obietnica zwrócona przez usługę updateCallback zostanie zrealizowana, przejście rozpocznie się w następnej klatce. Jeśli obietnica zwrócona przez funkcję updateCallback zostanie odrzucona, przejście zostanie przerwane.

Członkowie instancji ViewTransition:

viewTransition.updateCallbackDone

Obietnica, która następuje, gdy obietnica zwrócona przez funkcję updateCallback zostanie zrealizowana lub odrzucona, gdy zostanie odrzucona.

Interfejs View Transition API opakowuje zmianę DOM i tworzy przejście. Czasami jednak nie zależy Ci na powodzeniu czy porażce animacji przejścia, chcesz po prostu wiedzieć, czy i kiedy nastąpi zmiana DOM. Do tego celu służy właściwość updateCallbackDone.

viewTransition.ready

Obietnica, która spełnia się po utworzeniu pseudoelementów przejścia i za chwilę zacznie się animacja.

Jest odrzucana, jeśli nie można rozpocząć przenoszenia. Może to być spowodowane błędną konfiguracją, taką jak zduplikowane dyrektywy view-transition-name, lub zwróceniem odrzuconej obietnicy przez usługę updateCallback.

Przydaje się to do animowania pseudoelementów przejść za pomocą JavaScriptu.

viewTransition.finished

Obietnica, która następuje, gdy stan końcowy jest w pełni widoczny i interaktywny dla użytkownika.

Jest odrzucana tylko wtedy, gdy updateCallback zwraca odrzuconą obietnicę, ponieważ wskazuje to, że stan końcowy nie został utworzony.

W przeciwnym razie, jeśli przejście nie rozpocznie się lub zostanie pominięte, stan zakończenia nadal zostanie osiągnięty, więc wartość finished zostanie zrealizowana.

viewTransition.skipTransition()

Pomiń fragment przejścia z animacją.

Nie spowoduje to pominięcia wywołania updateCallback, ponieważ zmiana DOM jest niezależna od przejścia.

Domyślny styl i informacje o przejściu

::view-transition
Pseudoelement główny, który wypełnia widoczny obszar i zawiera wszystkie elementy ::view-transition-group.
::view-transition-group

Pozycjonowanie reklamy.

Przenosi wartości width i height między stanem „przed” i „po”.

Przejścia transform między kwadratem „przed” i „po” obszaru widocznego obszaru.

::view-transition-image-pair

Wypełnił całą grupę.

Zawiera isolation: isolate, aby ograniczyć efekt mieszania plus-lighter w starym i nowym widoku.

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

Jest zawsze w lewym górnym rogu opakowania.

Wypełnia 100% szerokości grupy, ale ma ustawioną automatyczną wysokość, więc zachowuje współczynnik proporcji, zamiast wypełniać grupę.

Zawiera mix-blend-mode: plus-lighter, który umożliwia rzeczywiste przenikanie.

Stary widok zostanie zmieniony z opacity: 1 na opacity: 0. Nowy widok zostanie przeniesiony z opacity: 0 do opacity: 1.

Prześlij opinię

Opinie deweloperów są na tym etapie bardzo ważne, dlatego zachęcamy do zgłaszania problemów na GitHubie wraz z sugestiami i pytaniami.