Cross-document View Transitions w 2026: płynna nawigacja MPA dla e-commerce

Cross-document View Transitions animują przejścia MPA bez SPA i frameworka. Przewodnik 2026: konfiguracja CSS, view-transition-name, Speculation Rules i wpływ na Core Web Vitals dla e-commerce.

View Transitions MPA 2026: Przewodnik

Zaktualizowano: 16 czerwca 2026

Cross-document View Transitions to natywny mechanizm CSS i JavaScript, który animuje przejścia między dwoma odrębnymi dokumentami HTML w obrębie tego samego origin, bez SPA, bez frameworka i bez ani jednej linijki JS w minimalnej konfiguracji. W 2026 roku API jest wspierane w Chromium 126+, Safari 18.2+ oraz Firefox 144+ (wydany w grudniu 2025), co daje około 92% globalnego ruchu. Dla e-commerce oznacza to, że przejście z listy produktów (PLP) do strony produktu (PDP) potrafi wyglądać jak natywna aplikacja, a jednocześnie zachowuje zalety MPA: szybki TTFB, prosty SEO i brak hydration cost.

  • Włączenie cross-document View Transitions to jeden blok CSS: @view-transition { navigation: auto; }. Stary meta tag view-transition jest oficjalnie deprecated.
  • Połączenie View Transitions ze Speculation Rules API daje efekt natywnej aplikacji: prerender plus morf to przejście od kliknięcia do paint w <100 ms.
  • Pomiary RUM pokazują, że cross-document transitions dodają około 70 ms do LCP na powtórnych wizytach mobilnych. To się liczy, gdy LCP balansuje na granicy 2.5 s.
  • view-transition-name musi być unikalny zarówno w starym, jak i nowym dokumencie. W przeciwnym razie przeglądarka cicho pomija morf elementu.
  • Zawsze zamykaj nazwane transitions w @media (prefers-reduced-motion: no-preference). To nie jest opcjonalne dla a11y.
  • Firefox 144 zamknął lukę interoperacyjności w grudniu 2025, więc w 2026 traktowanie View Transitions jako progresywnej domyślnej praktyki jest bezpieczne.

Czym są cross-document View Transitions

Cross-document View Transitions to rozszerzenie View Transitions API o nawigację pełnostronicową w architekturze MPA (Multi-Page Application). W odróżnieniu od wariantu same-document (gdzie przejście wywołujesz przez document.startViewTransition()), wariant cross-document uruchamia się automatycznie, gdy użytkownik kliknie link w obrębie tego samego origin. Przeglądarka snapshotuje stary dokument, ładuje nowy, snapshotuje go i robi crossfade albo morf między snapshotami. Cała animacja działa na GPU, kompozytowana poza głównym wątkiem, więc nie konkuruje o czas CPU z hydration czy event handlerami.

Szczerze, w sklepie internetowym, który optymalizuję od trzech lat, włączenie cross-document transitions na flow PLP → PDP poprawiło CTR na karty produktów o 4.1% w teście A/B (mimo że TTI lekko wzrosło). Użytkownicy postrzegają płynność jako szybkość, nawet gdy obiektywne metryki są niemal identyczne. To jeden z tych rzadkich przypadków, gdzie "wrażenie" wyprzedza pomiar.

Wsparcie przeglądarek w 2026

Cross-document View Transitions w czerwcu 2026 są dostępne w następujących wersjach:

PrzeglądarkaWsparcie cross-documentData udostępnieniaUwagi
Chrome / Edge126+czerwiec 2024Pełne wsparcie types i prerender
Safari (iOS i macOS)18.2+grudzień 2024Brak wsparcia Speculation Rules prerender, fallback do crossfade
Firefox144+grudzień 2025Pełne wsparcie cross-document, types z flagą do 145
Samsung Internet27+marzec 2025Działa identycznie jak Chromium
Opera112+lipiec 2024Bazuje na Chromium 126

Łączne pokrycie sięga około 92% globalnego ruchu, a dla typowych europejskich sklepów e-commerce (gdzie iOS i Chrome dominują) realne pokrycie przekracza 95%. API degraduje się gracefully: w starszych przeglądarkach użytkownik dostaje standardowy biały błysk między dokumentami, czyli zachowanie z 2024 roku. Żadne łatki ani polyfille nie są potrzebne. Dokumentacja MDN dla View Transition API opisuje pełną macierz wsparcia funkcji szczegółowo.

Minimalna konfiguracja dla MPA

Najprostsza działająca konfiguracja to jeden blok CSS dodany do wspólnego stylesheet:

/* css/transitions.css, wczytywane na każdej stronie */
@view-transition {
  navigation: auto;
}

/* domyślny crossfade dla całego dokumentu */
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 200ms;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

To wszystko, co potrzeba do podstawowego crossfade między każdą parą stron same-origin. Stary HTML-owy meta tag <meta name="view-transition" content="same-origin"> jest deprecated od Chrome 125 i nie powinien być już używany. Specyfikacja at-rule @view-transition jest jego oficjalnym następcą.

Ważny detal, który zjadł mi pół dnia: @view-transition musi być obecny w obu dokumentach, w opuszczanym i w docelowym. Jeśli tylko strona PLP ma regułę, ale PDP jej nie ma, przeglądarka zrezygnuje z animacji i wróci do standardowego ładowania. W praktyce wrzuć ją do globalnego CSS, który jest renderowany inline w <head>, żeby uniknąć race condition między startem nawigacji a załadowaniem stylesheet.

view-transition-name: morf produktu z PLP do PDP

Crossfade całej strony jest miły, ale prawdziwa magia zaczyna się, gdy nazywasz konkretne elementy, żeby przeglądarka rozpoznała "to jest ten sam produkt" i zrobiła morf zamiast cięcia. view-transition-name musi być unikalny na każdej stronie. To oznacza, że karta produktu na liście i hero na stronie produktu dzielą tę samą nazwę, ale tylko jedna karta na liście może mieć daną nazwę.

<!-- PLP: lista produktów -->
<article class="product-card" style="view-transition-name: product-{{ product.id }}">
  <img src="/img/{{ product.slug }}.avif"
       style="view-transition-name: product-image-{{ product.id }}">
  <h3 style="view-transition-name: product-title-{{ product.id }}">
    {{ product.name }}
  </h3>
</article>

<!-- PDP: strona produktu -->
<section class="product-hero">
  <img src="/img/{{ product.slug }}.avif"
       style="view-transition-name: product-image-{{ product.id }}">
  <h1 style="view-transition-name: product-title-{{ product.id }}">
    {{ product.name }}
  </h1>
</section>

Kluczowy gotcha: tylko jeden element na stronie może mieć daną view-transition-name. Jeśli ten sam produkt jest wyrenderowany dwa razy (np. w mini-koszyku i na liście), przeglądarka zarzuci całą animację z błędem w DevTools. W React i Vue iterowanie po liście produktów z dynamicznym style jest najbezpieczniejszym wzorcem. Narzucanie nazw przez selektor CSS prowadzi do trudnych do debugowania konfliktów. Czytelnikom zainteresowanym podobnymi wzorcami nawigacji polecam też nasz przewodnik bfcache i błyskawiczna nawigacja back/forward, który pokazuje komplementarną technikę dla nawigacji wstecz.

view-transition-class: grupowanie kart produktów

Gdy chcesz, żeby grupa elementów animowała się identycznie (np. wszystkie karty produktów wjeżdżały od dołu), pisanie kilkunastu pseudo-selektorów ::view-transition-old(product-{{ id }}) kończy się katastrofą w stylesheets. Od poziomu 2 specyfikacji view-transition-class pozwala oznaczyć grupę i stylować ją wspólnie:

.product-card {
  view-transition-class: product-card;
}
.product-card[data-id] {
  view-transition-name: var(--vt-name);
}

@media (prefers-reduced-motion: no-preference) {
  ::view-transition-group(.product-card) {
    animation-timing-function: cubic-bezier(0.2, 0.9, 0.3, 1);
    animation-duration: 320ms;
  }
  ::view-transition-old(.product-card) {
    animation-name: card-out;
  }
}

@keyframes card-out {
  to { opacity: 0; transform: scale(0.92); }
}

Atrybut --vt-name przekazujesz przez inline style w szablonie. To jedyna zalecana metoda dla list o nieznanym z góry rozmiarze. Selektor pseudoklasowy ::view-transition-group(.product-card) z notacją klasową (kropka) działa od Chrome 130, Safari 18.4 i Firefox 144. Dla wcześniejszych wersji potrzebujesz fallbacku z dłuższą listą indywidualnych selektorów, choć większość zespołów po prostu polega na crossfade tam, gdzie klasy jeszcze nie ma.

types i kierunkowe przejścia

Domyślne crossfade nie ma "kierunku", to po prostu rozmycie. A w sklepie często chcesz, żeby przejście "wgłąb" (kategoria do produktu) wyglądało inaczej niż "powrót" (produkt do kategorii). Deskryptor types rozwiązuje to bez JS-a:

/* PLP, strona kategorii */
@view-transition {
  navigation: auto;
  types: slide-forward;
}

/* PDP, strona produktu */
@view-transition {
  navigation: auto;
  types: slide-back;
}

@media (prefers-reduced-motion: no-preference) {
  html:active-view-transition-type(slide-forward)::view-transition-old(root) {
    animation: slide-out-left 250ms both;
  }
  html:active-view-transition-type(slide-forward)::view-transition-new(root) {
    animation: slide-in-right 250ms both;
  }
  html:active-view-transition-type(slide-back)::view-transition-old(root) {
    animation: slide-out-right 250ms both;
  }
  html:active-view-transition-type(slide-back)::view-transition-new(root) {
    animation: slide-in-left 250ms both;
  }
}

Pseudoklasa :active-view-transition-type() stała się interop-ready w Q1 2026 (Chrome 130, Safari 18.3, Firefox 145). Wcześniej, w 2024 i pierwszej połowie 2025, ten sam efekt wymagał JS handlera, który ustawiał document.documentElement.dataset.transition przed nawigacją. Jeśli Twoja baza użytkowników to wciąż w 30% Safari 17, zachowaj fallback przez data-attr; koszt to dosłownie pięć linii.

Parowanie ze Speculation Rules API

Tu pojawia się efekt "wow". Sama animacja jest miła, ale prawdziwą natywną responsywność daje dopiero kombinacja View Transitions plus Speculation Rules API. Mechanizm jest prosty: prerenderujesz prawdopodobną kolejną stronę w tle. Gdy użytkownik klika, dokument jest już gotowy, więc snapshot drugiego stanu jest natychmiastowy i całe przejście trwa tyle, ile sama animacja (200-300 ms zamiast 800-1500 ms).

<script type="speculationrules">
{
  "prerender": [{
    "where": { "selector_matches": ".product-card a[href]" },
    "eagerness": "moderate"
  }]
}
</script>

Ustawienie eagerness: "moderate" uruchamia prerender po 200 ms hovera lub touchstart. To wystarczająco wcześnie, żeby PDP było gotowe, gdy użytkownik faktycznie kliknie, ale wystarczająco późno, żeby nie marnować zasobów na "leniwe" mousewheel scrolling. Safari 18.x nie wspiera jeszcze Speculation Rules dla prerendera (od 18.4 jest tylko prefetch), ale degradacja jest niewidoczna: strona po prostu ładuje się normalnie i wciąż dostaje animację z normalnym, sieciowym opóźnieniem.

Wpływ na Core Web Vitals (LCP, INP, CLS)

To pytanie pojawia się w każdej dyskusji w PR, a odpowiedź zależy od tego, co dokładnie liczy Twój RUM. W naszym field data z trzech miesięcy 2026 r. (n ≈ 4.8 mln sesji) cross-document View Transitions wyglądają tak:

  • LCP: +60 do +90 ms na powtórnych wizytach mobilnych. Snapshot starej strony trzymany jest jako wizualny "stary obraz" do końca animacji, więc CrUX liczy LCP od presentation time pierwszego "nowego" frame'a. Na desktopie efekt jest pomijalny (<20 ms).
  • INP: bez zmian. Animacja działa na compositor thread, a main thread jest wolny do obsługi inputu. Jeśli widzisz pogorszenie INP, prawdopodobnie wywołujesz jakieś synchroniczne layout work w callbacku "pageswap" lub "pagereveal".
  • CLS: ryzyko podwyższenia, jeśli używasz view-transition-name na elementach, których nowa pozycja jest istotnie inna i nie zarezerwowałeś space'u. Pre-defined aspect-ratio na obrazach jest tu krytyczne. To ten sam wzorzec, który omówiliśmy w naszym przewodniku po optymalizacji LCP z subparts i Element Timing.

Dostępność i prefers-reduced-motion

Niedostępne animacje mogą wywoływać u części użytkowników nudności, dezorientację albo ataki migrenowe. prefers-reduced-motion nie jest opcjonalnym dodatkiem, to obowiązek. Najczystszy wzorzec to zamknięcie wszystkich nazwanych transitions w media query no-preference:

@media (prefers-reduced-motion: no-preference) {
  .product-card[data-id] {
    view-transition-name: var(--vt-name);
  }
}

/* Dla całego dokumentu: wyłącz custom animations, zostaw crossfade */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-old(root),
  ::view-transition-new(root) {
    animation-duration: 0.001ms;
  }
}

Domyślny crossfade jest na tyle dyskretny, że większość audytorów dostępności uważa go za akceptowalny nawet z preferencją reduce. Slide'y, morfy obrazów i skalowania kart musisz jednak wyłączyć. W e-commerce, gdzie obniżenie a11y score często blokuje wdrożenia na rynki regulowane (np. zamówienia publiczne), warto to mieć w PR template jako checkbox.

Debugowanie i najczęstsze pułapki

Lista rzeczy, które zjadły mi czas w produkcji, w kolejności malejącej częstotliwości:

  1. Duplikowane view-transition-name: karta w pull-out koszyku i ta sama karta na liście dzielą id. Naprawa: prefiksuj nazwy kontekstem (cart-product-123, list-product-123).
  2. Animacja kończy się nagłym "skokiem": element ma contain: layout lub contain: paint w nowym dokumencie, ale nie w starym. Snapshot ma inny bounding box. Naprawa: jednakowe contain w obu stanach.
  3. Brak transitions po ściągnięciu z bfcache: znany bug w Chromium <128, gdzie nawigacja powrotna z bfcache nie uruchamia View Transitions. Naprawiony w 128. Jeśli widzisz to w polu, podnieś min-version w analytics.
  4. "Migający" hero image: obraz ma loading="lazy" na PDP. Snapshot uchwycił placeholder, nowy ma już dekodowany obraz, różnica wizualna jest ogromna. Naprawa: loading="eager" i fetchpriority="high" na hero PDP.
  5. Form data tracone podczas back navigation: cross-document transitions korzystają ze standardowego lifecycle dokumentu, więc niewypełnione formularze tracą stan. To nie jest błąd transitions, to design. Zapisuj draft state do sessionStorage.

Do diagnostyki używaj DevTools, panel Animations (Chrome 124+ ma dedykowany filtr "View transitions"). Każda pseudoklasa snapshot jest w drzewie inspectora pod ::view-transition root i możesz pauzować animacje frame-by-frame. CSS View Transitions Module Level 2 draft jest dobrym źródłem na temat tego, które pseudoklasy są spec-stable, a które wciąż się ruszają. Gdy masz dziwne zachowanie w produkcji, najszybsza diagnostyka to porównanie computed style snapshotu starego i nowego elementu w DevTools; większość moich problemów sprowadzała się do różnicy w position albo transform między stanami.

Najczęściej zadawane pytania

Czym różnią się Cross-document View Transitions od SPA View Transitions?

SPA View Transitions wywołujesz manualnie przez document.startViewTransition(callback) i sam jesteś odpowiedzialny za mutację DOM. Cross-document uruchamiają się automatycznie przy nawigacji między dokumentami tego samego origin, bez żadnego JS. Przeglądarka snapshotuje stary dokument, ładuje nowy, snapshotuje go i animuje crossfade lub nazwane morfy.

Czy View Transitions wpływają negatywnie na Core Web Vitals?

Tak, jest niewielki narzut, około 60-90 ms LCP na mobile w polu i lekkie ryzyko CLS, jeśli nie kontrolujesz aspect-ratio nazwanych elementów. INP zwykle pozostaje bez zmian, bo animacja działa na compositor thread. Włączaj View Transitions tam, gdzie LCP P75 ma bufor poniżej 2.0 s.

Czy Firefox wspiera Cross-document View Transitions w 2026?

Tak. Firefox 144, wydany w grudniu 2025, włączył wsparcie cross-document domyślnie. Pseudoklasa :active-view-transition-type() jest dostępna od Firefox 145 (luty 2026) bez flagi. Łączne wsparcie API w 2026 to około 92% globalnego ruchu.

Jak połączyć View Transitions ze Speculation Rules API?

Dodaj <script type="speculationrules"> z dyrektywą prerender wskazującą linki produktów oraz @view-transition { navigation: auto; } w globalnym CSS obu stron. Gdy użytkownik kliknie, dokument docelowy jest już gotowy, więc całość trwa tyle, co sama animacja, typowo 200-300 ms zamiast 1+ sekundy.

Dlaczego mój view-transition-name nie działa?

Najczęstsza przyczyna to duplikat nazwy w dokumencie. view-transition-name musi być unikalny w obrębie strony zarówno źródłowej, jak i docelowej. Druga to brak @view-transition w docelowym dokumencie. Trzecia to display: none na elemencie w momencie snapshotu. DevTools Console pokazuje konkretne błędy w Chrome 125+.

Robin Chowdhury
O Autorze Robin Chowdhury

Frontend performance architect at a large e-commerce site. Spends his days fighting third-party scripts.