Przejścia widoku tego samego dokumentu w aplikacjach jednostronicowych

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

Aby aktywować przejście między widokiem tego samego dokumentu, wywołaj 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 rejestruje migawki wszystkich elementów, w których deklarowano właściwość CSS view-transition-name.

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

Są one następnie układane w drzewo pseudoelementów i animowane przy użyciu animacji CSS. Pary migawek ze starego i nowego stanu płynnie przechodzą ze starej pozycji i rozmiaru do nowej lokalizacji, a ich zawartość jest płynnie przekształcana. Jeśli chcesz, możesz dostosować animacje za pomocą CSS.


Przejście domyślne: przenikanie

Domyślne przejście widoku jest przenikanie, więc jest dobrym wprowadzeniem 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 stan. Możesz to zrobić w dowolny sposób. Możesz na przykład dodawać i usuwać elementy, zmieniać nazwy klas czy zmieniać style.

I tak oto strony przechodzą płynnie w siebie:

Domyślny przenikanie. Minimalna wersja demonstracyjna. Źródło.

Przejścia nie są aż tak imponujące. Na szczęście przejścia można dostosowywać, ale najpierw musisz zrozumieć, jak działa to podstawowe przejście krzyżowe.


Jak działają te przejścia

Zaktualizujmy poprzedni przykład kodu.

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

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

Po zakończeniu wywoływana jest funkcja wywołania zwrotnego przekazana do .startViewTransition(). To właśnie tam zmienia się DOM. Następnie interfejs API rejestruje nowy stan strony.

Po zarejestrowaniu nowego stanu interfejs API tworzy drzewo pseudoelementów 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 znajduje się w nakładce nad całą pozostałą treścią strony. Jest to przydatne, jeśli chcesz ustawić kolor tła dla przejścia.

::view-transition-old(root) to zrzut ekranu starego widoku, a ::view-transition-new(root) to opublikowana reprezentacja nowego widoku. Oba są renderowane jako „zastępowana zawartość” CSS (np. <img>).

Stary widok jest animowany od opacity: 1 do opacity: 0, a w nowym jest wyświetlany od opacity: 0 do opacity: 1, co powoduje przenikanie.

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

Dostosowywanie przejścia

Za pomocą CSS można kierować wszystkie pseudoelementy przejścia między widokami, a animacje są definiowane przy użyciu tego języka, można je modyfikować za pomocą istniejących właściwości animacji CSS. Na przykład:

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

Po tej jednej zmianie zanikanie jest teraz bardzo wolne:

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

To nadal nie robi wrażenia. Zamiast tego ten kod implementuje przejście wspólnej osi interfejsu 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;
}

A oto wynik:

Przejście na współdzieloną oś. Minimalna wersja demonstracyjna. Źródło.

Przenoszenie wielu elementów

W poprzednim pokazie demo cała strona była objęta przejściem z wspólnym zastosowaniem osi. To działa na większości strony, ale nie pasuje do nagłówka, ponieważ przesuwa się, a potem znów pojawia.

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

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

Wartość view-transition-name może być dowolna (z wyjątkiem none, co oznacza, że nie ma nazwy przejścia). Służy do jednoznacznego identyfikowania elementu na każdym etapie przejścia.

Efekt:

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

Teraz nagłówek pozostaje na miejscu i przechodzi w kolejny.

Ta deklaracja CSS spowodowała zmianę drzewa pseudoelementów:

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

Dostępne są teraz 2 grupy przenoszenia. Jeden dla nagłówka, a drugi dla reszty. Można na nie kierować reklamy niezależnie za pomocą CSS i stosować różne przejścia. W tym przypadku main-header ma domyślne przejście, czyli przejście krzyżowe.

OK, domyślne przejście to nie tylko przejście – ::view-transition-group również przechodzi:

  • Położenie i transformacja (za pomocą transform)
  • Szerokość
  • Wysokość

Do tej pory nie miało to znaczenia, ponieważ nagłówek ma ten sam rozmiar i znajduje się po obu stronach zmiany DOM. Możesz też wyodrębnić tekst w nagłówku:

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

fit-content jest używany, dzięki czemu element ma rozmiar tekstu, zamiast rozciągać się do pozostałej szerokości. W przeciwnym razie strzałka wstecz zmniejsza rozmiar elementu tekstowego nagłówka, a nie taki sam rozmiar na obu stronach.

Mamy więc do zagrania trzy części:

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

Wróćmy do ustawień domyślnych:

Przesuwanie tekstu nagłówka. Minimalna wersja demonstracyjna. Źródło.

Teraz tekst nagłówka przesuwa się, aby zrobić miejsce dla przycisku Wstecz.


Animowanie wielu pseudoelementów w ten sam sposób za pomocą funkcji view-transition-class

Obsługa przeglądarek

  • Chrome: 125.
  • Edge: 125.
  • Firefox: funkcja nieobsługiwana.
  • Safari: nieobsługiwane.

Załóżmy, że masz przejście w widoku z kilkoma kartami i tytułem na stronie. Aby animować wszystkie karty oprócz tytułu, musisz napisać selektor, który będzie kierować na każdą 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? To 20 selektorów, które musisz napisać. Chcesz dodać nowy element? Potem musisz też rozwinąć selektor, który stosuje style animacji. Niezbyt skalowalny.

Wartości view-transition-class można używać w pseudoelementach przejścia widoku, aby zastosować 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);
}

Ten przykład kart wykorzystuje poprzedni fragment kodu CSS. Wszystkie karty, w tym nowo dodane, mają takie samo ustawienie czasu, które jest stosowane za pomocą jednego selektora: html::view-transition-group(.card).

Nagranie prezentacji kart. Użycie funkcji view-transition-class powoduje zastosowanie tego samego elementu animation-timing-function do wszystkich kart z wyjątkiem dodanych lub usuniętych.

Debugowanie przejść

Przejścia widoku są oparte na animacjach CSS, dlatego panel Animacje w Narzędziach deweloperskich w Chrome świetnie nadaje się do debugowania przejść.

W panelu Animacje możesz wstrzymać kolejną animację, a potem przewinąć ją w przód i w tył. W tym czasie pseudoelementy przejścia będą widoczne 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 użyliśmy view-transition-name do utworzenia oddzielnych elementów przejścia dla nagłówka i tekstu w nagłówku. Zasadniczo są to te same elementy przed zmianą DOM i po niej, ale można tworzyć przejścia, gdy jest inaczej.

Na przykład dla głównego umieszczonego filmu można zastosować view-transition-name:

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

Następnie, gdy klikniesz miniaturę, możesz przypisać do niej ten sam view-transition-name, ale tylko na czas przejścia:

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

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

A oto wynik:

Przejście jednego elementu w drugi. Minimalna wersja demonstracyjna. Źródło.

Miniatura przechodzi teraz w obraz główny. Chociaż są to koncepcyjnie (i dosłownie) różne elementy, interfejs API przejść traktuje je jako tę samą rzecz, ponieważ mają ten sam element view-transition-name.

Rzeczywisty kod przejścia jest nieco bardziej skomplikowany niż poprzedni przykład, ponieważ obsługuje również przejście z powrotem do strony miniatury. Pełne informacje o wdrożeniu znajdziesz w źródle kodu.


Niestandardowe przejścia wejścia i wyjścia

Rozważ ten przykład:

Otwieranie i zamykanie paska bocznego. Minimalna wersja demonstracyjna. Źródło.

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

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

W przeciwieństwie do nagłówka w poprzednim przykładzie pasek boczny nie jest jednak widoczny na wszystkich stronach. Jeśli w obu stanach jest 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) tam nie będzie. Ponieważ nie ma „starego” obrazu na potrzeby paska bocznego, para obrazów będzie zawierać tylko ::view-transition-new(sidebar). Podobnie, jeśli pasek boczny znajduje się tylko na starej stronie, para obrazów będzie zawierać tylko ::view-transition-old(sidebar).

W poprzednim pokazie pasek boczny zmieniał się w zależności od tego, czy się pojawiał, znikał czy był widoczny w obu stanach. Pojawia się, przesuwając się z prawej strony i zwiększając, znika, przesuwając się w prawo i znikając, oraz pozostaje na miejscu, gdy jest obecny w obu stanach.

Aby utworzyć konkretne przejścia wejściowe i wyjściowe, możesz użyć pseudoklasy :only-child, aby ustawić kierowanie na stare lub nowe pseudoelementy, jeśli są one jedynymi elementami podrzędnymi 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, w którym pasek boczny jest dostępny w obu stanach, ponieważ ustawienie domyślne jest idealne.

Asynchroniczne aktualizacje DOM i oczekiwanie na treści

Callback przekazany do .startViewTransition() może zwrócić obietnicę, która umożliwia asynchroniczne aktualizacje DOM i czekanie na przygotowanie ważnych treści.

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

Przenoszenie nie rozpocznie się, dopóki nie spełnisz obietnicy. W tym czasie strona jest zablokowana, więc opóźnienia należy ograniczyć do minimum. W szczególności pobieranie danych z sieci należy wykonać przed wywołaniem funkcji .startViewTransition(), gdy strona jest wciąż w pełni interaktywna, a nie w ramach wywołania zwrotnego .startViewTransition().

Jeśli chcesz poczekać na gotowość obrazów lub czcionek, pamiętaj, by ustawić limit czasu agresywnego:

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łkiem tego uniknąć i wykorzystać treści, które już masz.


Wykorzystanie potencjału treści, które już masz

W przypadku, gdy miniatura przechodzi do większego obrazu:

Miniatura przechodzi do większego obrazu. Wypróbuj witrynę demonstracyjną

Domyślnym przejściem jest przenikanie, co oznacza, że miniatura może zanikać z niewczytanym jeszcze pełnym obrazem.

Jednym ze sposobów na rozwiązanie tego problemu jest oczekiwanie na załadowanie całego obrazu przed rozpoczęciem przejścia. Najlepiej zrobić to przed wywołaniem funkcji .startViewTransition(), aby strona pozostała interaktywna, a użytkownik mógł zobaczyć wskaźnik postępu ładowania. Jednak w tym przypadku istnieje lepsze rozwiązanie:

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

Miniatura nie znika, ale znajduje się pod pełnym zdjęciem. Oznacza to, że jeśli nowy widok nie został jeszcze załadowany, miniatura jest widoczna przez cały czas przełączania. Oznacza to, że przejście może się rozpocząć od razu, a pełny obraz może się wczytać w dowolnym momencie.

To nie zadziałałoby, gdyby nowy widok zawierał przejrzystość. Tym razem wiemy, że tak nie jest, dlatego 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 jest w formacie 1:1, a zdjęcie główne ma proporcje 16:9?

Przejście jednego elementu w drugi z zmianą formatu obrazu. Minimalna wersja prezentacji. Źródło.

W domyślnym przejściu grupa animuje się od rozmiaru przed do rozmiaru po. Stary i nowy widok mają 100% szerokości grupy i automatyczną wysokość, co oznacza, że współczynnik proporcji jest utrzymywany niezależnie od wielkości grupy.

To jest dobra domyślna wartość, ale w tym przypadku nie jest ona odpowiednia. 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 w środku elementu, gdy jego szerokość się zwiększa, ale pełny obraz nie jest „przycinany” podczas przejścia z formatu 1:1 na 16:9.

Więcej informacji znajdziesz w artykule Przejścia między widokami: obsługa zmian formatu obrazu.


Używaj zapytań o multimedia do zmiany przejść dla różnych stanów urządzenia

Na urządzeniach mobilnych i komputerach możesz używać różnych przejść. W tym przykładzie na urządzeniu mobilnym występuje pełne przesunięcie z boków, a na komputerze – bardziej subtelne przesunięcie:

Przejście jednego elementu w inny. Minimalna wersja demonstracyjna. Źródło.

W tym celu użyj 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ć elementy, do których przypisujesz view-transition-name w zależności od dopasowania zapytań dotyczących multimediów.


Reagowanie na „zmniejszony ruch” preferencja

Użytkownicy mogą za pomocą systemu operacyjnego wskazać, że preferują ograniczenie ruchu. Ustawienie to jest ujawnione 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;
  }
}

Ustawienie „Ograniczony ruch” nie oznacza jednak, że użytkownik chce, aby brakowało ruchu. Zamiast poprzedniego fragmentu możesz wybrać bardziej subtelną animację, która wciąż odzwierciedla relacje między elementami i przepływ danych.


Obsługa wielu stylów przejść między widokami za pomocą typów przejść

Obsługa przeglądarek

  • Chrome: 125.
  • Edge: 125.
  • Firefox: funkcja nieobsługiwana.
  • Safari: nieobsługiwane.

Czasami przejście z jednego widoku do drugiego wymaga specjalnego dostosowania. Na przykład podczas przechodzenia do następnej lub poprzedniej strony w sekwencji stron możesz przesuwać zawartość w różnym kierunku w zależności od tego, czy przechodzisz do strony wyższej czy niższej w sekwencji.

Nagranie prezentacji strony Używa różnych przejść w zależności od tego, na którą stronę się przenosisz.

W tym celu możesz użyć typów przejścia widoku, które umożliwiają przypisanie co najmniej 1 typu do aktywnego przejścia widoku. Na przykład podczas przechodzenia na wyższą wersję strony w sekwencji podziału na strony używaj typu forwards, a podczas przechodzenia na niższą stronę – typu backwards. Te typy animacji są aktywne tylko przy przechwytywaniu lub wykonywaniu przejścia. Każdy typ można dostosować za pomocą CSS, tak aby uzyskać różne animacje.

Aby używać typów w przejściu do widoku tego samego dokumentu, przekazujesz parametr types do metody startViewTransition. Aby to umożliwić, funkcja document.startViewTransition przyjmuje 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 rodzaje zapytań, użyj selektora :active-view-transition-type(). Przekaż do selektora type, na który chcesz kierować reklamy. Dzięki temu style wielu przejść widoku można oddzielić od siebie, a deklaracje jednego z nich nie będą kolidować z deklaracjami drugiego.

Typy są stosowane tylko podczas rejestrowania lub wykonywania przejścia, więc za pomocą selektora możesz ustawić lub odznaczyć view-transition-name w elemencie 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 tym demo paginacji zawartość strony przesuwa się do przodu lub do tyłu w zależności od tego, do której strony się przenosisz. Typy reklam są określane na podstawie kliknięcia, po którym są przekazywane do parametru document.startViewTransition.

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

html:active-view-transition {
    
}

Obsługa wielu stylów przejść z użyciem nazwy klasy w poziomie głównym przejść widoku

Czasami przejście z jednego określonego typu widoku na inny powinno być odpowiednio dostosowane. lub „wstecz” nawigacja nie powinna się różnić nawigacji.

Różne przejścia w przypadku „wstecz”. Minimalna wersja demonstracyjna. Źródło.

Przed wprowadzeniem typów przejść w takich przypadkach tymczasowo ustawiano nazwę klasy w korzeniach przejść. W przypadku wywołania metody document.startViewTransition ten poziom główny przejścia to element <html> dostępny za pomocą metody document.documentElement w języku 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');
}

Aby usunąć klasy po zakończeniu przejścia, w tym przykładzie użyto elementu transition.finished, czyli obietnicy wygasającej po osiągnięciu stanu końcowego. Inne właściwości tego obiektu opisano w dokumentacji interfejsu API.

Możesz teraz użyć 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ż służyć do zmiany, które elementy otrzymują view-transition-name.


Uruchamiaj przejścia bez blokowania innych animacji

Spójrz na tę demonstrację zmiany pozycji przejścia w filmie:

Przejście w filmie. Minimalna wersja prezentacji. Źródło.

Czy coś jest w nim nie tak? Nie przejmuj się, jeśli tak się nie stało. Tutaj jest spowolnione:

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

W trakcie przejścia film się zawiesza, a potem odtwarzana jest jego wersja. Dzieje się tak, ponieważ ::view-transition-old(video) to zrzut ekranu starego widoku, a ::view-transition-new(video) to żywy obraz nowego widoku.

Możesz to naprawić, ale najpierw zastanów się, czy warto to robić. Jeśli nie widzisz informacji o problemie gdy przejście było odtwarzane z normalną szybkością, nie chciałbym go zmieniać.

Jeśli naprawdę chcesz rozwiązać ten problem, nie pokazuj ::view-transition-old(video). przełącz się prosto do usługi ::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;
}

To wszystko.

Przejście między filmami w wolniejszym tempie. Minimalna wersja demonstracyjna. Źródło.

Film jest teraz odtwarzany podczas przejścia.


Animacja za pomocą JavaScript

Do tej pory wszystkie przejścia były definiowane za pomocą CSS, ale czasami CSS nie wystarcza:

Przejście okręgiem Minimalna wersja prezentacji. Źródło.

Niektórych elementów tej zmiany nie można wdrożyć za pomocą samego CSS:

  • Animacja rozpoczyna się od miejsca kliknięcia.
  • Na końcu animacji okrąg ma promień do najdalszego rogu. Mamy jednak nadzieję, że jest to możliwe w przyszłości w usłudze 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 wykorzystano obietnicę transition.ready, która wygasa po utworzeniu pseudoelementów przejścia. Inne właściwości tego obiektu opisano w dokumentacji interfejsu API.


Przejścia jako ulepszenie

Interfejs View Przenoszenie API został zaprojektowany do „zawijania” zmianę DOM i utworzenie dla niej przejścia. Jednak przejście powinno być traktowane jako ulepszenie, ponieważ aplikacja nie powinna przechodzić w stan „błąd”, jeśli zmiana DOM-u się powiedzie, ale przejście nie powiedzie się. W idealnej sytuacji przeniesienie nie powinno się zakończyć niepowodzeniem, ale jeśli do niego dojdzie, nie powinno to negatywnie wpłynąć na wrażenia użytkownika.

Aby traktować przejścia jako ulepszenie, uważaj, aby nie korzystać z obietnic przejścia w sposób, który mógłby spowodować, że w razie niepowodzenia przejścia aplikacja się zakończy.

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 z tym przykładem polega na tym, że switchView() zostanie odrzucone, jeśli przejście nie może osiągnąć stanu ready, ale nie oznacza to, że nie udało się przełączyć widoku. DOM mógł zostać zaktualizowany, ale wystąpiły zduplikowane view-transition-name, więc 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żyto metody transition.updateCallbackDone do oczekiwania na aktualizację DOM i odrzucenia go w przypadku niepowodzenia. switchView nie odrzuca już przejścia, gdy się nie powiedzie, rozwiązuje je po zakończeniu aktualizacji DOM i odrzuca, gdy się nie powiedzie.

Jeśli chcesz, aby switchView kończyło się, gdy nowy widok „się ustabilizuje”, czyli gdy wszystkie animowane przejścia zostaną ukończone lub pominięte, zastąp transition.updateCallbackDone wartością transition.finished.


Nie jest to polyfill, ale…

Korzystanie z kodu polyfill nie jest łatwe. Ta funkcja ułatwia pracę w przeglądarkach, które nie obsługują przejścia 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 ten 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ą przejść z widokiem, polecenie updateDOM będzie nadal wywoływane, ale przejście nie będzie animowane.

Możesz też podać classNames, które zostaną dodane do <html> podczas przejścia, co ułatwi zmianę przejścia w zależności od typu nawigacji.

Jeśli nie chcesz korzystać z animacji, możesz też przekazać true do skipTransition, nawet w przeglądarkach, które obsługują przejścia między widokami. Jest to przydatne, jeśli w Twojej witrynie użytkownicy mogą wyłączyć przejścia.


Praca z ramami

Jeśli pracujesz z biblioteką lub platformą, która wyodrębnia zmiany DOM, trudno jest stwierdzić, kiedy zmiana DOM zostaje zakończona. Oto kilka przykładów, które korzystają z powyższej pomocy na różnych platformach.

  • React – klucz to flushSync, który stosuje synchronicznie zbiór zmian stanu. Tak, pojawia się duże ostrzeżenie dotyczące używania tego interfejsu API, ale Dan Abramov zapewnia mi, że w tym przypadku jest on odpowiedni. Jak zwykle w przypadku kodu React i kodu asynchronicznego podczas korzystania z różnych obietnic zwracanych przez funkcję startViewTransition sprawdź, czy kod działa w prawidłowym stanie.
  • Vue.js – kluczowym elementem jest tutaj nextTick, który jest wykonywany po zaktualizowaniu DOM.
  • Svelte – bardzo podobny do Vue, z tą różnicą, że metodą oczekiwania na następną zmianę jest tick.
  • Lit – kluczem jest tutaj obietnica this.updateComplete w komponentach, która zostaje zrealizowana po zaktualizowaniu DOM.
  • Angular – klucz to applicationRef.tick, który usuwa oczekujące zmiany DOM. Od wersji 17 Angulara możesz użyć withViewTransitions, który jest dołączony do @angular/router.

Dokumentacja API

const viewTransition = document.startViewTransition(update)

Utwórz nowe ViewTransition.

update to funkcja, która jest wywoływana po zarejestrowaniu bieżącego stanu dokumentu.

Następnie, gdy spełni się obietnica zwrócona przez funkcję updateCallback, przejście rozpocznie się w następnej klatce. Jeśli obietnica zwrócona przez użytkownika updateCallback zostanie odrzucona, przeniesienie zostanie anulowane.

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

Utwórz nowe ViewTransition z wybranymi typami

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

types określa aktywne typy przejścia podczas ich przechwytywania. Początkowo jest puste. Więcej informacji znajdziesz w sekcji viewTransition.types.

Użytkownicy instancji ViewTransition:

viewTransition.updateCallbackDone

Obietnica, która jest spełniona, gdy obietnica zwrócona przez updateCallback jest spełniona, lub odrzucona, gdy jest odrzucona.

Interfejs View Migrate API opakowuje zmianę DOM i tworzy przejście. Czasem jednak nie zależy Ci na sukcesie lub niepowodzeniu animacji przejścia, być może chcesz po prostu wiedzieć, czy i kiedy nastąpi zmiana DOM. updateCallbackDone jest przeznaczony do tego zastosowania.

viewTransition.ready

Obietnica, która zostaje zrealizowana po utworzeniu pseudoelementów przejścia i za chwilę rozpocznie się animacja.

Jeśli nie można rozpocząć przejścia, zostanie odrzucone. Może to być spowodowane błędną konfiguracją, na przykład duplikatem view-transition-name, lub jeśli updateCallback zwraca odrzuconą obietnicę.

Jest to przydatne w przypadku animowania pseudoelementów przejścia za pomocą JavaScriptu.

viewTransition.finished

Obietnica spełniająca się, gdy stan końcowy jest w pełni widoczny i interaktywny dla użytkownika.

Odrzuca tylko wtedy, gdy updateCallback zwraca odrzuconą obietnicę, co oznacza, że stan końcowy nie został utworzony.

W przeciwnym razie, jeśli przejście się nie rozpocznie lub zostanie w jego trakcie pominięte, stan końcowy nadal będzie miał miejsce, więc finished realizuje zamierzony cel.

viewTransition.types

Obiekt podobny do obiektu Set, który zawiera typy przejść widoku. Aby manipulować wpisami, użyj metod instancji clear(), add() i delete().

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

Typy są automatycznie usuwane po zakończeniu przejścia między widokami.

viewTransition.skipTransition()

Pomiń animację przejścia.

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


Domyślny styl i odniesienie do przejścia

::view-transition
Pseudoelement rdzeń, który wypełnia widok i zawiera poszczególne elementy ::view-transition-group.
::view-transition-group

Właściwe pozycjonowanie.

Przejścia width i height między stanami „przed” i „po”.

Przejścia transform między kwadratem „przed” i „po” w przestrzeni widoku.

::view-transition-image-pair

Pozycjonowane, aby wypełnić grupę.

Musisz isolation: isolate, aby ograniczyć wpływ mix-blend-mode na stare i nowe wyświetlenia.

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

Jest umieszczony całkowicie w lewym górnym rogu opakowania.

Wypełnia 100% szerokości grupy, ale ma automatyczną wysokość, więc zachowa proporcje, zamiast wypełniać grupę.

Musi zawierać mix-blend-mode: plus-lighter, aby umożliwić płynne przejście.

Stary widok zostanie zmieniony z opacity: 1 na opacity: 0. Nowe widoki przechodzą z poziomu opacity: 0 na poziom opacity: 1.


Prześlij opinię

Opinie deweloperów są dla nas bardzo ważne. Aby to zrobić, prześlij zgłoszenie do grupy roboczej CSS na GitHubie, podając sugestie i pytania. Dodaj do problemu prefiks [css-view-transitions].

Jeśli napotkasz błąd, zgłoś błąd w Chromium.