Przejścia widoku tego samego dokumentu w aplikacjach jednostronicowych

Opublikowano: 17 sierpnia 2021 r., ostatnia aktualizacja: 25 września 2024 r.

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 wywołać przejście do widoku tego samego dokumentu, wywołaj funkcję 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 zrzuty wszystkich elementów, które mają zadeklarowaną właściwość CSS view-transition-name.

Następnie wykonuje przekazaną funkcję z argumentem zwrotnym, która aktualizuje DOM, a potem wykonuje zrzuty ekranu 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 w nowym rozmiarze do nowej lokalizacji, a zawartość strony zmienia się. Jeśli chcesz, możesz dostosować animacje za pomocą kodu CSS.


Domyślne przejście: przejście krzyżowe

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

W ten sposób strony przechodzą przez przenikanie:

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 płynne.


Jak działają te przejścia

Zaktualizujmy poprzedni przykładowy kod.

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

Po wywołaniu .startViewTransition() interfejs API przechwytuje bieżący stan strony. Dotyczy to również robienia zrzutów ekranu.

Po zakończeniu wywołanie zwrotne przekazane do .startViewTransition() jest wywoływane. Właśnie wtedy zmienia się DOM. Następnie interfejs API rejestruje nowy stan strony.

Po przechwyceniu nowego stanu interfejs API tworzy pseudoelementowe drzewo w następujący sposób:

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

::view-transition znajduje się w przeźroczystej nakładce na wszystkie inne elementy na stronie. 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

Wszystkie pseudoelementy przejść widoku mogą być kierowane za pomocą CSS, a ponieważ animacje są definiowane za pomocą CSS, możesz 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;
}

Dzięki tej zmianie przejście jest teraz bardzo powolne:

Długi efekt przejścia. Minimalna wersja prezentacji. Ź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;
}

Oto wynik:

Przejście na współdzieloną oś. Minimalna wersja prezentacji. Ź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. Sprawdza się to w przypadku większości stron, ale nie wygląda to dobrze w przypadku nagłówka, ponieważ wysuwa się, aby z powrotem wsunąć się z powrotem.

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

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

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

W efekcie:

Przejście na wspólną oś z niezmiennym nagłówkiem. Minimalna wersja demonstracyjna. Źródło.

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

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. Jedna w nagłówku, a druga w odniesieniu do pozostałych. Można je kierować niezależnie za pomocą CSS i przypisać do nich 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:

  • Pozycjonowanie i przekształcanie (za pomocą elementu transform)
  • Szerokość
  • Wysokość

Do tej pory nie miało to znaczenia, ponieważ nagłówek ma taki sam rozmiar i pozycję 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;
}

Wartość fit-content sprawia, że element ma rozmiar tekstu, a nie rozciąga się do pozostałej szerokości. Bez tego strzałka wstecz zmniejsza rozmiar elementu tekstu nagłówka, a nie zachowuje ten 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:

Tekst przesuwanego nagłówka. Minimalna wersja prezentacji. Ź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: nieobsługiwane.
  • Wersja testowa technologii Safari: obsługiwana.

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? Musisz też rozszerzyć 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 korzysta z poprzedniego fragmentu kodu CSS. Wszystkie karty (w tym nowo dodane) mają taki sam czas wyświetlania z jednym selektorem: html::view-transition-group(.card).

Nagranie prezentacji kart. Za pomocą view-transition-class można zastosować te same 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 wstecz. W tym czasie pseudoelementy przejścia znajdziesz w panelu Elementy.

Debugowanie przejść widoku za pomocą Narzędzi deweloperskich w Chrome.

Elementy w przechodzeniu 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 główny element wideo może mieć view-transition-name:

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

Następnie po kliknięciu miniatury można mu nadać taki sam view-transition-name, tylko na czas przejścia:

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

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

Wynik:

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

Miniatura przechodzi teraz w obraz główny. Mimo że są to elementy o różnym charakterze (i dosłownie), interfejs API przejścia traktuje je jako ten sam element, ponieważ mają ten sam identyfikator view-transition-name.

Prawdziwy kod tego przejścia jest nieco bardziej skomplikowany niż w poprzednim przykładzie, ponieważ obsługuje też przejście z powrotem na stronę miniatur. Zajrzyj do źródła, aby zapoznać się z pełną implementacją.


Niestandardowe przejścia

Spójrz na ten przykład:

Otwieranie i opuszczanie 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 wyświetla się jednak 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 jest widoczny tylko na nowej stronie, pseudoelement ::view-transition-old(sidebar) nie będzie tam obecny. Nie ma „starego” obrazu na pasku bocznym, para obrazów będzie zawierać tylko ::view-transition-new(sidebar). 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. Wchodzi, przesuwa się od prawej strony i zanika. Znika, przesuwając w prawo i zanika. Pozostaje na swoim 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 ukierunkować stare lub nowe pseudoelementy, gdy jest to jedyny element podrzędny 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ść

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 obietnica nie zostanie zrealizowana. W tym czasie strona jest zablokowana, więc opóźnienia należy ograniczyć do minimum. W szczególności pobieranie danych z sieci powinno odbywać się przed wywołaniem funkcji .startViewTransition(), gdy strona jest jeszcze w pełni interaktywna, a nie w ramach wywołania zwrotnego .startViewTransition().

Jeśli zdecydujesz się poczekać na obrazy lub czcionki, użyj krótkiego limitu 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ł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 przejście płynne, co oznacza, że miniatura może być płynnie zastąpiona pełnym obrazem, który jeszcze się nie załadował.

Możesz na przykład poczekać, aż załaduje się cały obraz, zanim rozpoczniesz przejście. Najlepiej zrobić to przed wywołaniem funkcji .startViewTransition(), aby strona pozostała interaktywna, a użytkownik mógł zobaczyć wskaźnik postępu ładowania. Ale w tym przypadku jest 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ł wczytany, miniatura jest widoczna przez cały okres przejścia. Oznacza to, że przejście może się rozpocząć od razu, a pełny obraz może się wczytać w dowolnym momencie.

Nie byłoby to możliwe, gdyby nowe widok zawierał przezroczystość, ale w tym przypadku wiemy, że tak nie jest, więc możemy dokonać optymalizacji.

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 format 1:1, a obraz główny 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”. W obu widokach szerokość grupy jest ustawiona na 100%, a wysokość na automatyczną, co oznacza, że format pozostaje niezmieniony niezależnie od rozmiaru grupy.

To dobre ustawienie domyślne, ale w tym przypadku nie jest to wymagane. 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 przy poszerzaniu szerokości miniatura pozostaje na środku elementu, ale cały obraz zostaje cofnięty. gdy przechodzi się od 1:1 do 16:9.

Więcej informacji znajdziesz w artykule Przejścia między widokami: obsługa zmian współczynnika proporcji.


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 drugi. 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ą wskazać, że wolą ograniczone animacje w systemie operacyjnym, a ta preferencja jest wyświetlana w CSS.

Możesz uniemożliwić przenoszenie 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 powyższego fragmentu kodu możesz wybrać bardziej subtelną animację, która nadal będzie wyrażać 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: 18.

Czasami przejście z jednego widoku do drugiego wymaga specjalnego dostosowania. Gdy na przykład przechodzisz na następną lub poprzednią stronę w sekwencji podziału na strony, możesz przesuwać treść w różnym kierunku w zależności od tego, czy przechodzisz na wyższą, czy na niższą stronę z sekwencji.

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

Możesz do tego użyć typów przejścia między widokami, które umożliwiają przypisanie co najmniej 1 typu do aktywnego przejścia między widokami. Jeśli na przykład przechodzisz do wyższej strony w sekwencji podziału na strony, użyj typu forwards, a jeśli przechodzisz do niższej strony, użyj 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 widoku tego samego dokumentu, przekaż 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 zareagować na te typy, użyj selektora :active-view-transition-type(). Przekaż do selektora type, na który chcesz kierować reklamy. Dzięki temu możesz zachować style wielu przejść między widokami oddzielnie od siebie, bez nakładania się deklaracji jednych na deklaracje innych.

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 tej prezentacji o podziale na strony zawartość strony przesuwa się do przodu lub do tyłu w zależności od otwieranego numeru strony. 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 do 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. Inną opcją jest zastosowanie innej ikony dla przycisku „Wstecz” niż dla przycisku „Dalej”.

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ścia. Podczas wywoływania funkcji document.startViewTransition ten element <html> jest korzeniami przejścia, do których można uzyskać dostęp za pomocą funkcji 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');
}

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 opisane są w dokumentacji interfejsu API.

Teraz możesz użyć tej nazwy klasy w kodzie 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 służyć do zmiany elementów, które mają mieć view-transition-name.


Uruchamiaj przejścia bez blokowania innych animacji

Obejrzyj ten film demonstrujący przenoszenie pozycji:

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

Czy zauważyłeś/zauważyłaś coś nieprawidłowego? Nie martw się, jeśli nie. Oto spowolnione nagranie:

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

Podczas przejścia film wydaje się być zamrożony, a potem powoli zaczyna się odtwarzać. Dzieje się tak, ponieważ ::view-transition-old(video) to zrzut ekranu starego widoku, a ::view-transition-new(video) to na żywo 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 to naprawić, nie pokazuj ::view-transition-old(video), tylko od razu przejdź do ::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.


Integracja z interfejsem API Nawigacji (i innymi frameworkami)

Przejścia widoku danych określa się w taki sposób, aby można je było zintegrować z innymi platformami lub bibliotekami. Jeśli na przykład aplikacja jednostronicowa (SPA) korzysta z przekaźnika, możesz dostosować mechanizm aktualizacji przekaźnika, aby aktualizować treści za pomocą przejścia widoku.

W tym fragmencie kodu pochodzącym z tego demonstracyjnego interfejsu stronowania obsługa przechwytywania interfejsu Navigation API jest dostosowana do wywołania funkcji document.startViewTransition, gdy obsługiwane są przejścia między widokami.

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

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

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

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

Niektóre przeglądarki (ale nie wszystkie) oferują własne przejścia, gdy użytkownik przesuwa palcem, aby się poruszać. W takim przypadku nie należy uruchamiać własnego przejścia z widoku witryny, ponieważ mogłoby to spowodować złe lub mylące wrażenia użytkowników. Użytkownik zobaczy 2 przejścia – jedna po drugiej przez przeglądarkę i druga przez Ciebie.

Dlatego zalecamy, aby uniemożliwić rozpoczęcie przejścia po wyświetleniu, jeśli przeglądarka zapewni własne przejście wizualne. Aby to zrobić, sprawdź wartość właściwości hasUAVisualTransition instancji NavigateEvent. Właściwość jest ustawiona na true, gdy przeglądarka obsługuje przejście wizualne. Właściwość hasUIVisualTransition występuje też w przypadku instancji PopStateEvent.

W poprzednim fragmencie kodu sprawdzanie, które decyduje, czy należy wykonać przejście widoku, uwzględnia tę właściwość. Jeśli nie ma obsługi przejść w widoku tego samego dokumentu lub jeśli przeglądarka zapewniła już własne przejście, przejście widoku jest pomijane.

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

W tym nagraniu użytkownik przesuwa palcem, aby wrócić do poprzedniej strony. Zrzut po lewej stronie nie zawiera sprawdzania flagi hasUAVisualTransition. Na nagraniu po prawej stronie znajduje się potwierdzenie, więc użytkownik może pominąć ręczną zmianę widoku, ponieważ przeglądarka zapewnia wizualne przejście.

Porównanie tej samej witryny bez sprawdzenia źródła „hasUAVisualTransition” (po lewej) i jego szerokości (po prawej)

Animowanie z użyciem JavaScriptu

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

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

Niektórych części tego przejścia nie da się zrealizować za pomocą samego CSS:

  • Animacja rozpoczyna się od miejsca kliknięcia.
  • Animacja kończy się, gdy okrąg ma promień do najdalszego narożnika. Mamy jednak nadzieję, że w przyszłości będzie to możliwe 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 użyto transition.ready, czyli obietnicy, która zostanie spełniona 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. Przejście należy jednak traktować jako ulepszenie – aplikacja nie może zawierać błędu w przypadku, gdy zmiana DOM się powiedzie, ale przejście się nie powiedzie. 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, nie używaj obietnic przejścia w sposób, który powodowałby awarię aplikacji w przypadku niepowodzenia przejścia.

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. Moduł switchView nie odrzuca już konwersji w przypadku niepowodzenia, koryguje po zakończeniu aktualizacji DOM i odrzuca w przypadku niepowodzenia.

Jeśli chcesz, by reguła switchView uwzględniała tylko zmiany w nowym widoku (np. wszystkie animowane przejścia zostały ukończone lub pominięte do końca), zastąp transition.updateCallbackDone elementem 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 wykorzystać 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ść między widokami, funkcja updateDOM będzie nadal wywoływana, ale nie będzie animowanego przejścia.

Możesz też podać classNames do dodania do elementu <html> podczas przejścia, co ułatwi jego zmianę w zależności od rodzaju nawigacji.

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


Praca z platformami

Jeśli korzystasz z biblioteki lub frameworka, który abstrahuje od zmian w DOM, trudnością jest ustalenie, kiedy zmiany w DOM są zakończone. Oto kilka przykładów, w których wykorzystano pomagacza powyżej w różnych ramach.

  • React – kluczem jest tutaj flushSync, który synchronicznie stosuje zestaw 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 – klucz to nextTick, który jest realizowany 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 Angular w wersji 17 możesz używać usługi withViewTransitions, która zawiera @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 updateCallback jest odrzucana, przejście zostaje anulowane.

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

utworzyć nowe ViewTransition z wybranymi typami,

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

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

Członkowie instancji ViewTransition:

viewTransition.updateCallbackDone

Obietnica, która jest spełniona, gdy obietnicę zwracaną 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 do animowania pseudoelementów przejścia za pomocą JavaScriptu.

viewTransition.finished

Obietnice, które są spełnione, gdy stan końcowy jest w pełni widoczny i interaktywny dla użytkownika.

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

Jeśli jednak przejście nie zostanie rozpoczęte lub zostanie pominięte, stan końcowy zostanie osiągnięty, więc finished zostanie spełnione.

viewTransition.types

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

Aby odpowiedzieć na określony typ w CSS, użyj selektora pseudoklasy :active-view-transition-type(type) w korzeniach przejścia.

Typy są automatycznie czyszczone 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
Główny pseudoelement, który wypełnia widoczny obszar i zawiera wszystkie elementy ::view-transition-group.
::view-transition-group

Właściwe pozycjonowanie.

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

Przenosi o transform między elementem „przed” i „po” poczwórny obszar widocznego obszaru.

::view-transition-image-pair

idealnie nadaje się do wypełnienia grupy.

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

Ma wartość mix-blend-mode: plus-lighter, która umożliwia rzeczywiste przenikanie.

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ć, zgłoś problem w grupie roboczej ds. usług porównywania cen na GitHubie, przesyłając sugestie i pytania. Dodaj do problemu prefiks [css-view-transitions].

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