Transisi Lintas Dokumen yang Konsisten

Masalah

Transisi tampilan lintas dokumen menganimasikan elemen di antara dua halaman selama navigasi origin yang sama. Browser mengambil snapshot halaman lama, melakukan navigasi, lalu menganimasikan dari snapshot ke halaman baru. Jika halaman baru belum selesai memuat resource penting — stylesheet, skrip tata letak, atau elemen DOM utama — transisi akan dianimasikan ke status yang tidak lengkap atau tanpa gaya. Hal ini menyebabkan gangguan visual seperti elemen berubah ke posisi yang salah, konten mengalir ulang di tengah animasi, atau font pengganti berkedip ke font web setelah transisi selesai.

Solusi

Gunakan blocking="render" pada elemen <link> dan <script> penting di <head> halaman baru, dan gunakan <link rel="expect"> untuk memblokir rendering hingga elemen DOM tertentu telah diuraikan. Hal ini memastikan browser tidak memulai animasi transisi tampilan hingga status visual halaman baru stabil. Browser akan terus mengurai HTML di latar belakang — hanya rendering yang ditangguhkan.

Strategi Penerapan

  1. WAJIB: Pilih ikut serta dalam transisi tampilan lintas dokumen dengan at-rule CSS @view-transition di kedua halaman.
  2. WAJIB: Pastikan stylesheet penting berada di <head>. Stylesheet di <head> memblokir rendering secara default. Stylesheet yang dimasukkan secara dinamis memerlukan blocking="render" eksplisit.
  3. LAKUKAN penggunaan blocking="render" pada elemen <script> yang harus dieksekusi sebelum transisi dianimasikan (misalnya, skrip yang menerapkan tema atau memengaruhi tata letak).
  4. LAKUKAN penggunaan <link rel="expect" href="#element-id" blocking="render"> untuk memblokir rendering hingga konten paruh atas selesai diuraikan. Hal ini berlaku untuk semua jenis transisi: cross-fade halaman penuh (untuk menghindari animasi ke halaman kosong), animasi morph (untuk memastikan elemen bernama ada di DOM), dan tata letak yang bergantung pada skrip (untuk memastikan konten bergaya diuraikan).
  5. JANGAN memblokir rendering pada konten yang tidak penting. Hanya blokir resource dan elemen yang memengaruhi area pandang awal. Pemblokiran pada terlalu banyak konten akan menunda transisi dan menurunkan performa yang dirasakan.

Panduan Penerapan

Langkah 1: Aktifkan Transisi Tampilan Lintas Dokumen

WAJIB: Halaman sumber dan tujuan harus menyertakan aturan-at @view-transition. Tanpa ini, transisi lintas dokumen tidak akan terjadi.

/*
  MANDATORY: Include this rule in every page that participates
  in cross-document view transitions.
  `navigation: auto` enables transitions for standard navigations
  (link clicks, form submissions, back/forward).
*/
@view-transition {
  navigation: auto;
}

/* MANDATORY Copy-Paste Safety: Disable cross-document view transitions for users requesting reduced motion */
@media (prefers-reduced-motion: reduce) {
  @view-transition {
    navigation: none;
  }
}

Langkah 2: Blokir Rendering Hingga Skrip Penting Dieksekusi

Jika skrip non-pemblokiran di <head> harus berjalan sebelum transisi dianimasikan (misalnya, untuk menerapkan class tema atau memengaruhi tata letak), tandai dengan blocking="render". Tanpa ini, skrip async, defer, atau type="module" dapat dijalankan setelah transisi dimulai.

<head>
  <!--
    DO: Mark layout-critical scripts with blocking="render".
  -->
  <script type=module blocking="render">
    // Example: apply a stored theme before the page renders,
    // so the transition snapshot reflects the correct theme.
    document.documentElement.dataset.theme =
      localStorage.getItem('theme') || 'light';
  </script>
</head>

Langkah 3: Blokir Rendering Hingga Elemen DOM Utama Diurai

Stylesheet dan skrip blocking="render" di <head> hanya menjamin bahwa <head> telah diproses sepenuhnya. Skrip ini tidak menunggu konten <body> diuraikan. Tanpa pemblokiran tambahan, browser dapat mengambil snapshot halaman baru sebelum elemen paruh atas ada di DOM — sehingga menghasilkan transisi yang dianimasikan ke halaman kosong atau yang dirender sebagian.

<link rel="expect"> mengatasi masalah ini dengan memblokir rendering hingga elemen tertentu (diidentifikasi oleh id-nya) telah diuraikan. Nilai href harus berupa ID fragmen (misalnya, #hero) yang cocok dengan atribut id elemen target. Setelah tag penutup elemen tersebut diurai, blok render akan dirilis. Perhatikan bahwa <link rel="expect"> hanya menjamin bahwa elemen DOM target ada dalam dokumen — elemen ini tidak menunggu subresource eksternal (seperti piksel <img>) didownload atau didekode.

GUNAKAN <link rel="expect"> dalam semua skenario berikut:

Kasus Penggunaan 1: Cross-Fade Halaman Penuh

Meskipun tidak ada elemen individual yang memiliki view-transition-name, transisi root default akan melakukan cross-fade pada seluruh halaman. Jika snapshot halaman baru diambil sebelum konten paruh atas diuraikan, cross-fade akan menganimasikan dari halaman lama ke halaman kosong atau tidak lengkap. Memblokir rendering pada elemen yang menandai akhir konten paruh atas yang terlihat.

<head>
  <link rel="stylesheet" href="/css/styles.css">

  <!--
    DO: Block rendering until the main content area is parsed,
    even for a simple cross-fade. Without this, the browser may
    snapshot the page before visible content exists in the DOM,
    causing the cross-fade to reveal a blank or partial page.
  -->
  <link rel="expect" href="#main-content" blocking="render">
</head>
<body>
  <header>...</header>
  <main id="main-content">
    <h1>Page Title</h1>
    <p>Above-the-fold content the user should see immediately.</p>
  </main>
  <!-- Content below the fold does NOT need to be blocked on -->
  <section>...</section>
</body>

Kasus Penggunaan 2: Animasi Perubahan Bentuk Antara Elemen Tertentu

Jika elemen di kedua halaman memiliki view-transition-name yang sama, browser akan mengubahnya dengan lancar di seluruh navigasi. Jika elemen target belum diuraikan saat transisi dimulai, browser tidak dapat menemukannya — morph akan menurun menjadi animasi keluar dan masuk yang terpisah. Memblokir rendering hingga elemen dengan view-transition-name telah diurai.

Karena <link rel="expect"> hanya menjeda rendering hingga elemen DOM target diuraikan dan tidak menunggu data gambar didownload atau didekode, <img> yang tidak di-cache dapat diambil sebagai persegi panjang kosong dalam snapshot halaman baru. Untuk mencegah snapshot kosong atau pergeseran tata letak selama perubahan gambar, lakukan pramuat atau pra-dekode gambar transisi penting (misalnya, dengan <link rel="preload" as="image"> atau dengan melakukan pra-rendering halaman tujuan) dan berikan penggantian ukuran eksplisit (atribut width dan height atau aspect-ratio CSS, plus warna latar belakang placeholder).

<head>
  <link rel="stylesheet" href="/css/styles.css">

  <!--
    DO: Preload critical transition images so their pixels are
    available to decode before the new-page snapshot is captured.
    <link rel="expect"> only waits for the DOM element to be parsed,
    NOT for image pixels to download or decode.
  -->
  <link rel="preload" as="image" href="/img/product.webp" fetchpriority="high">

  <!--
    DO: Block rendering until the element participating in the
    morph animation has been parsed. Without this, the browser
    may start the transition before #hero exists, causing the
    morph to degrade to a fade-out/fade-in.
  -->
  <link rel="expect" href="#hero" blocking="render">

  <!--
    When multiple blocking="render" resources are present,
    rendering is blocked until ALL of them are satisfied.
    Here, the browser waits for both the script to execute
    AND the #hero element to be parsed — whichever comes last.
  -->
  <script async blocking="render" src="/js/transition-setup.js"></script>
</head>
<body>
  <header>...</header>
  <section id="hero">
    <h1 style="view-transition-name: page-title">Product Name</h1>
    <!--
      DO: Provide explicit dimensions (width/height or aspect-ratio)
      as a sizing fallback so the snapshot captures the correct layout
      box even if image decoding is still in progress.
    -->
    <img
      style="view-transition-name: hero-image"
      src="/img/product.webp"
      width="800"
      height="600"
      alt="Product"
    >
  </section>
</body>

Langkah 4: Gunakan Kueri Media untuk Pemblokiran Rendering yang Responsif

Ukuran area pandang yang berbeda dapat menampilkan jumlah konten yang berbeda di paruh atas. Gunakan atribut media di <link rel="expect"> untuk memblokir rendering hanya untuk konten yang terlihat pada lebar area tampilan tertentu.

<head>
  <!--
    DO: Use media queries to conditionally block rendering.
    On wide screens, both the hero and the sidebar are visible,
    so block until both are parsed. On narrow screens, only the
    hero is visible initially.
  -->
  <link
    rel="expect"
    href="#hero"
    blocking="render"
    media="screen and (width <= 768px)"
  >
  <link
    rel="expect"
    href="#sidebar"
    blocking="render"
    media="screen and (width > 768px)"
  >
</head>

Langkah 5: Gunakan pagereveal untuk Transisi yang Bergantung pada Konteks (Opsional)

Peristiwa pagereveal tidak diperlukan untuk strategi pemblokiran render inti. Transisi ini hanya diperlukan jika nilai view-transition-name harus ditetapkan secara dinamis berdasarkan tempat pengguna melakukan navigasi — misalnya, mengubah item daftar tertentu menjadi judul halaman detail.

Jika nilai view-transition-name ditetapkan secara statis di CSS, atau jika Anda hanya menggunakan cross-fade halaman penuh default, lewati langkah ini sepenuhnya.

<head>
  <!--
    MANDATORY: The pagereveal listener must be registered before
    the page renders. Inline small setup scripts with blocking="render"
    to avoid an extra network round-trip on cold loads, or use an
    external async script with blocking="render" so the listener is
    registered early without blocking HTML parsing.
    If the listener is registered too late (e.g., in a deferred
    script), the event may have already fired.
  -->
  <script async blocking="render" src="/js/transition-setup.js"></script>
</head>
// transition-setup.js
window.addEventListener('pagereveal', async (event) => {
  if (!event.viewTransition) return;

  const from = navigation.activation?.from;
  if (!from) return;

  const fromUrl = new URL(from.url);

  // DO: Assign view-transition-name based on navigation context.
  // This enables a morph animation from the product card on the
  // list page to the heading on the detail page.
  if (fromUrl.pathname === '/products/') {
    const heading = document.querySelector('main h1');
    if (!heading) return;

    heading.style.viewTransitionName = 'product-title';

    try {
      await event.viewTransition.ready;
    } catch (error) {
      // Skipped view transitions reject `ready` with InvalidStateError.
      if (error.name !== 'InvalidStateError') {
        throw error;
      }
    } finally {
      // MANDATORY: Remove the temporary name after the transition
      // finishes. Stale names interfere with subsequent navigations
      // and prevent the page from entering the bfcache.
      await event.viewTransition.finished;
      heading.style.viewTransitionName = '';
    }
  }
});

Praktik Terbaik

  • LAKUKAN penetapan view-transition-name melalui CSS jika memungkinkan. Cadangkan penetapan JavaScript (melalui pagereveal) untuk kasus saat nama bergantung pada konteks navigasi.
  • LAKUKAN: pastikan skrip pemblokir render tetap kecil dan cepat. Browser memiliki waktu tunggu bawaan (sekitar 4 detik), setelah itu transisi akan dilewati sepenuhnya (menolak viewTransition.ready dengan InvalidStateError jika transisi dimulai).
  • JANGAN gunakan <link rel="expect"> untuk memblokir elemen yang berada di bagian dalam halaman yang tidak terlihat di area tampilan awal. Hal ini akan menunda transisi tanpa manfaat visual.
  • JANGAN menandai pemuat analisis, jaringan iklan, pengelola tag, atau pemuat pihak ketiga lainnya dengan blocking="render", meskipun pemuat tersebut memasukkan konten ke area pandang awal. Latensi jaringan mereka diperhitungkan dalam waktu tunggu transisi dan output-nya bukan bagian dari status stabil halaman.
  • JANGAN tetapkan view-transition-name yang sama ke beberapa elemen di halaman yang sama. Nama duplikat menyebabkan seluruh transisi dilewati.
  • Dampak Pengaturan Waktu Teknologi Pendukung: Penggunaan blocking="render" akan menunda pembaruan visual dan render awal. Meskipun hal ini mencegah gangguan visual bagi pengguna yang dapat melihat, hal ini dapat menyebabkan latensi pemrosesan atau inisialisasi yang tertunda untuk pembaca layar dan teknologi pendukung lainnya yang bergantung pada hierarki aksesibilitas yang dirender. Pertimbangkan manfaat kontinuitas visual dibandingkan dengan latensi pembacaan awal untuk pengguna non-visual, dan pastikan skrip pemblokiran render minimal dan sangat dioptimalkan.

Strategi Penggantian

Transisi tampilan lintas dokumen adalah kandidat yang sangat baik untuk progressive enhancement. Di browser yang tidak mendukungnya, aturan @view-transition akan diabaikan dan navigasi origin yang sama standar akan terjadi persis seperti tanpa fitur tersebut. Browser yang mendukung akan mendapatkan transisi yang lancar; browser lainnya akan mendapatkan navigasi standar. Dukungan browser yang terbatas bukanlah alasan untuk menghindari adopsi.

Semua browser yang mendukung transisi tampilan lintas dokumen juga mendukung blocking="render" dan <link rel="expect">, sehingga tidak diperlukan penggantian terpisah untuk fitur pemblokiran render yang dijelaskan dalam panduan ini.

Pertimbangan Lainnya

  1. Dampak Performa: Setiap resource yang memblokir render akan menunda dimulainya animasi transisi tampilan. Minimalkan jumlah skrip yang memblokir render dan gunakan <link rel="expect"> hanya untuk elemen yang berada di paruh atas. Lakukan pra-rendering halaman tujuan menggunakan Speculation Rules API untuk menghilangkan penundaan pemuatan sepenuhnya.
  2. Perilaku Waktu Tunggu: Jika waktu pemblokiran render gabungan melebihi sekitar 4 detik, browser akan melewati transisi (dan promise viewTransition.ready yang aktif akan ditolak dengan InvalidStateError). Pastikan resource penting dimuat dengan baik dalam periode ini.
  3. Kompatibilitas bfcache: Penetapan view-transition-name sementara yang tidak dibersihkan setelah transisi dapat mencegah halaman memasuki bfcache. Selalu hapus nama yang ditetapkan secara dinamis di callback finished.