Przejścia widoku tego samego dokumentu w aplikacjach jednostronicowych

Przejście w widoku pojedynczego dokumentu jest nazywane przejściem widoku tego samego dokumentu. Zwykle dzieje się tak w aplikacjach jednostronicowych (SPA), w których do aktualizowania DOM używany jest JavaScript. Przejścia widoku tego samego dokumentu są obsługiwane w Chrome od wersji 111.

Aby aktywować przejście w widoku tego samego dokumentu, wywołaj polecenie document.startViewTransition:

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

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

Po wywołaniu przeglądarka automatycznie przechwytuje zrzuty wszystkich elementów, które mają zadeklarowaną właściwość CSS view-transition-name.

Następnie wykonuje przekazane wywołanie zwrotne, które aktualizuje DOM, a potem wykonuje zrzuty nowego stanu.

Migawki te są następnie układane w drzewo złożone z pseudoelementów i animowane za pomocą animacji CSS. Pary zrzutów ze starego i nowego stanu płynnie przechodzą ze starego i nowego rozmiaru do nowej lokalizacji, podczas gdy ich zawartość się zmienia. Jeśli chcesz, możesz dostosować animacje za pomocą CSS.


Domyślne przejście: Przenikanie

Domyślne przejście w widoku może być przenikaniem, 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 zrobić w dowolny sposób. Można na przykład dodawać i usuwać elementy, zmieniać nazwy klas oraz style.

I w ten sposób strony przenikają:

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

Przenikanie nie jest aż tak imponujące. Na szczęście przejścia można dostosowywać, ale najpierw musisz zrozumieć, jak działa ta podstawowa metoda stopniowego zanikania.


Jak przebiega przenoszenie kont

Zaktualizujmy poprzedni przykładowy kod.

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

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

Po 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 nowego stanu interfejs API tworzy pseudoelementowe drzewo podobne do następującego:

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

Dostosuj przejście

Wszystkie pseudoelementy przejścia widoku mogą być kierowane za pomocą CSS, a ponieważ animacje są zdefiniowane za pomocą CSS, możesz je modyfikować za pomocą dotychczasowych 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 ten kod implementuje przejście współdzielonej osi 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.

Rozumiem. Domyślne przejście to nie tylko przenikanie – ::view-transition-group również się zmienia:

  • Ustaw i przekształć (za pomocą typu 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żesz 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. Jeśli ta zasada nie jest skonfigurowana, strzałka wstecz zmniejsza rozmiar elementu tekstu nagłówka, a nie taki sam rozmiar na obu stronach.

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.


Animuj wiele pseudoelementów w ten sam sposób dzięki funkcji view-transition-class

Obsługa przeglądarek

  • 125
  • 125
  • x
  • x

Załóżmy, że masz przejście do widoku obejmujące kilka kart, ale także tytuł na stronie. Aby animować wszystkie karty oprócz tytułu, musisz utworzyć selektor, który będzie kierowany do poszczególnych kart.

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

Masz 20 elementów? Trzeba zapisać 20 selektorów. Dodać nowy element? Musisz też powiększyć selektor, który stosuje style animacji. Ustawienie nie jest skalowalne.

Elementu view-transition-class można używać w pseudoelementach przejścia widoku, aby stosować tę samą regułę stylu.

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

Poniższy przykład kart wykorzystuje poprzedni fragment kodu CSS. Wszystkie karty, w tym nowo dodane, mają ten sam czas działania z jednym selektorem: html::view-transition-group(.card).

Nagrywanie prezentacji kart. Jeśli korzystasz z view-transition-class, ta sama wartość (animation-timing-function) jest stosowana do wszystkich kart oprócz tych dodanych lub usuniętych.

Debuguj 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ść widoku 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 umieszczonym głównym filmem może być 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 tego przejścia jest nieco bardziej skomplikowany od poprzedniego przykładu, 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 poprzedniej wersji demonstracyjnej pasek boczny przechodzi w różny sposób w zależności od tego, czy otwiera się, zamyka czy jest w obu miejscach. 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 stare lub nowe pseudoelementy, gdy są to jedyne elementy podrzędne 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.


W pełni wykorzystaj treści, które już masz

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

Miniatura zmienia się w większy obraz. Wypróbuj stronę demonstracyjną.

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ługuj zmiany 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 zgodne z oczekiwaniami. 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.

Bardziej szczegółowe informacje znajdziesz na stronie (Zobacz przejścia: obsługa zmian współczynnika proporcji)(https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/)


Używaj zapytań o multimedia, aby zmieniać przejścia dla różnych stanów urządzenia

Możesz użyć różnych przejść na komórce i komputerze, jak w tym przykładzie, który pokazuje 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.


Reagowanie na ustawienie „zmniejszony ruch”

Użytkownicy mogą wskazać, że wolą zmniejszony ruch w systemie operacyjnym i to ustawienie jest przedstawione w 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 poprzedniego fragmentu możesz wybrać bardziej subtelną animację, która nadal odzwierciedla związek między elementami i przepływ danych.


Obsługa wielu stylów przejścia widoku z typami przejść widoków

Czasami przejście z jednego widoku do innego powinno wymagać ściśle dostosowanego przejścia. Jeśli na przykład przechodzisz do następnej lub poprzedniej strony w sekwencji podziału na strony, możesz przesunąć zawartość w różnym kierunku w zależności od tego, czy przejdziesz na wyższą czy na niższą stronę sekwencji.

Nagrywanie demonstracji podziału na strony. Wykorzystuje różne przejścia w zależności od strony, na którą wchodzisz.

Do tego celu możesz użyć typów przejść widoku, które pozwalają przypisać jeden lub kilka typów do przejścia w Widoku aktywnym. Na przykład podczas przechodzenia na wyższą stronę w sekwencji podziału na strony użyj typu forwards, a na niższych stronach – typu backwards. Te typy są aktywne tylko podczas przechwytywania lub wykonywania przejścia, a każdy z nich można dostosować za pomocą CSS, aby używać różnych animacji.

Aby używać typów w przejściu w widoku tego samego dokumentu, musisz przekazać atrybut types do metody startViewTransition. Aby na to zezwolić, document.startViewTransition akceptuje też obiekt: update to funkcja wywołania zwrotnego, która aktualizuje DOM, a types to tablica z typami.

const direction = determineBackwardsOrForwards();

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

Aby odpowiedzieć na te typy, użyj selektora :active-view-transition-type(). Przekaż do selektora wartość type, na którą chcesz kierować reklamy. Dzięki temu możesz oddzielić style wielu widoków, a deklaracje jednego z nich nie będą ingerowały w deklaracje drugiego.

Typy mają zastosowanie tylko podczas przechwytywania lub wykonywania przejścia, więc za pomocą selektora możesz ustawić (lub cofnąć ustawienie) view-transition-name elementu tylko w przypadku przejścia widoku tego typu.

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

W tej prezentacji podziału na strony zawartość strony przesuwa się do przodu lub do tyłu w zależności od numeru strony, którą przeglądasz. Typy są określane przy kliknięciu, które jest przekazywane do funkcji document.startViewTransition.

Aby kierować reklamy na dowolne przejście w Widoku aktywnym, niezależnie od typu, możesz użyć selektora pseudoklasy :active-view-transition.

html:active-view-transition {
    …
}

Obsługuj wiele stylów przejścia widoku, korzystając z nazwy klasy w głównym widoku przejścia

Czasami przejście z jednego typu widoków danych do innego powinno być ściśle dostosowane do jego potrzeb. Nawigacja „wstecz” powinna być inna niż „do przodu”.

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

Przed typami przejść sposób obsługi takich przypadków polegał na tymczasowym ustawieniu nazwy klasy w katalogu głównym przejścia. W przypadku wywołania document.startViewTransition tenm przejściem głównym jest element <html> dostępny za pomocą document.documentElement w 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');
}

Do usunięcia klas po zakończeniu przenoszenia używany jest w tym przykładzie transition.finished, który znika po osiągnięciu stanu końcowego. 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.


Uruchamiaj 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 wolno
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 jest to łatwa funkcja w przypadku kodu polyfill. Jednak ta funkcja pomocnicza znacznie ułatwia pracę w przeglądarkach, które nie obsługują przechodzenia między widokami:

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

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

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

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

  // …
}

W przeglądarkach, które nie obsługują przechodzenia widoku, funkcja updateDOM nadal będzie wywoływana, 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 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(update)

Rozpocznij nowe ViewTransition.

update to funkcja, która jest wywoływana po przechwyceniu 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.

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

Rozpocznij nowy element ViewTransition o określonych typach

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

types określa aktywne typy przejścia podczas jego rejestrowania lub wykonywania. Początkowo jest puste. Więcej informacji znajdziesz w sekcji viewTransition.types poniżej.

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 udanej czy nieudanej 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.types

Obiekt podobny do Set, który zawiera typy przejścia w widoku aktywnym. Do manipulowania wpisami użyj jej metod instancji clear(), add() i delete().

Aby odpowiedzieć na określony typ w CSS, użyj pseudoklasy :active-view-transition-type(type) na głównej stronie przejścia.

Typy są automatycznie czyszczone po zakończeniu przejścia w widoku danych.

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ć wpływ metody mix-blend-mode na stary i nowy widok.

::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ą dla nas bardzo ważne. Aby to zrobić, zgłoś problem grupie roboczej usługi porównywania cen na GitHubie, podając sugestie i pytania. Dodaj prefiks [css-view-transitions] do problemu.

Jeśli natrafisz na błąd, zgłoś błąd Chromium.