Transisi yang lancar dan sederhana dengan View Transitions API

Jake Archibald
Jake Archibald

Dukungan Browser

  • 111
  • 111
  • x
  • x

Sumber

View Transition API memudahkan perubahan DOM dalam satu langkah, saat membuat transisi animasi di antara dua status. Fitur ini tersedia di Chrome 111+.

Transitions dibuat dengan View Transition API. Coba situs demo – Memerlukan Chrome 111+.

Mengapa kami memerlukan fitur ini?

Transisi halaman tidak hanya terlihat bagus, tetapi juga mengomunikasikan arah alur, dan memperjelas elemen mana yang terkait dari halaman ke halaman. Hal ini bahkan dapat terjadi selama pengambilan data, sehingga menghasilkan persepsi performa yang lebih cepat.

Namun, kita sudah memiliki alat animasi di web, seperti transisi CSS, animasi CSS, dan Web Animation API, jadi mengapa kita memerlukan hal baru untuk memindahkan berbagai hal?

Yang benar adalah, transisi status itu sulit, bahkan dengan alat yang sudah kita miliki.

Bahkan sesuatu seperti cross-fade sederhana melibatkan kedua status yang ada secara bersamaan. Hal tersebut menimbulkan tantangan kegunaan, seperti menangani interaksi tambahan pada elemen keluar. Selain itu, untuk pengguna perangkat asistif, ada periode ketika status sebelum dan sesudah berada di DOM secara bersamaan, dan segala sesuatunya dapat bergerak di sekitar pohon dengan cara yang baik secara visual, namun bisa dengan mudah menyebabkan posisi membaca dan fokus hilang.

Menangani perubahan status akan sangat sulit jika kedua status tersebut berbeda dalam posisi scroll. Selain itu, jika elemen berpindah dari satu container ke container lainnya, Anda dapat mengalami kesulitan dengan overflow: hidden dan bentuk clipping lainnya, yang berarti Anda harus menyusun ulang CSS untuk mendapatkan efek yang diinginkan.

Bukan tidak mungkin, ini hanya sangat sulit.

Transisi Tampilan memberi Anda cara yang lebih mudah, dengan memungkinkan Anda membuat perubahan DOM tanpa tumpang tindih antar-status, tetapi membuat animasi transisi antar-status menggunakan tampilan yang di-snapshot.

Selain itu, meskipun penerapan saat ini menargetkan aplikasi web satu halaman (SPA), fitur ini akan diperluas untuk memungkinkan transisi antarpemuatan halaman penuh, yang saat ini tidak mungkin dilakukan.

Status standardisasi

Fitur ini sedang dikembangkan di Grup Kerja CSS W3C sebagai spesifikasi draf.

Setelah puas dengan desain API, kami akan memulai proses dan pemeriksaan yang diperlukan untuk menjadikan fitur ini stabil.

Masukan developer sangat penting, jadi laporkan masalah di GitHub dengan saran dan pertanyaan.

Transisi paling sederhana: Cross-fade

Transisi Tampilan default adalah cross-fade, sehingga berfungsi sebagai pengantar yang bagus untuk 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));
}

Tempat updateTheDOMSomehow mengubah DOM ke status baru. Hal itu bisa dilakukan sesuka Anda: Menambah/menghapus elemen, mengubah nama kelas, mengubah gaya... tidak masalah.

Dan seperti itu, halaman akan memudar bersilang:

Cross-fade default. Demo minimal. Sumber.

Oke, cross-fade tidak begitu mengesankan. Untungnya, transisi dapat disesuaikan, tetapi sebelum sampai di sana, kita perlu memahami cara kerja cross-fade dasar ini.

Cara kerja transisi ini

Mengambil contoh kode dari atas:

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

Ketika .startViewTransition() dipanggil, API akan merekam status halaman saat ini. Hal ini termasuk mengambil screenshot.

Setelah selesai, callback yang diteruskan ke .startViewTransition() akan dipanggil. Di situlah DOM berubah. Kemudian, API akan merekam status baru halaman.

Setelah status direkam, API membuat hierarki elemen pseudo seperti ini:

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

::view-transition berada di overlay, di atas semua hal lain pada halaman. Hal ini berguna jika Anda ingin menyetel warna latar belakang untuk transisi.

::view-transition-old(root) adalah screenshot tampilan lama, dan ::view-transition-new(root) adalah representasi langsung tampilan baru. Keduanya dirender sebagai CSS 'replaced content' (seperti <img>).

Tampilan lama dianimasikan dari opacity: 1 ke opacity: 0, sementara tampilan baru dianimasikan dari opacity: 0 ke opacity: 1, yang membuat cross-fade.

Semua animasi dilakukan menggunakan animasi CSS, sehingga dapat disesuaikan dengan CSS.

Penyesuaian sederhana

Semua elemen semu di atas dapat ditargetkan dengan CSS, dan karena animasi ditentukan menggunakan CSS, Anda dapat memodifikasinya menggunakan properti animasi CSS yang ada. Contoh:

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

Dengan satu perubahan tersebut, fade kini menjadi sangat lambat:

Cross-fade panjang. Demo minimal. Sumber.

Oke, itu masih belum mengesankan. Sebagai gantinya, mari kita terapkan transisi sumbu bersama Desain Material:

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

Dan inilah hasilnya:

Transisi sumbu merata. Demo minimal. Sumber.

Mentransisi beberapa elemen

Dalam demo sebelumnya, seluruh halaman terlibat dalam transisi sumbu merata. Cara ini berfungsi untuk sebagian besar halaman, tetapi tampaknya tidak cukup tepat untuk judul karena dapat bergeser keluar hanya untuk bergeser kembali masuk.

Untuk menghindari hal ini, Anda dapat mengekstrak {i>header<i} dari bagian lain halaman sehingga dapat dianimasikan secara terpisah. Hal ini dilakukan dengan menetapkan view-transition-name ke elemen.

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

Nilai view-transition-name dapat berupa apa pun yang Anda inginkan (kecuali untuk none, yang berarti tidak ada nama transisi). Atribut ini digunakan untuk mengidentifikasi elemen di seluruh transisi secara unik.

Dan hasilnya:

Transisi sumbu merata dengan header tetap. Demo minimal. Sumber.

Sekarang header tetap berada di tempatnya dan memudar bersilang.

Deklarasi CSS tersebut menyebabkan hierarki elemen pseudo berubah:

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

Sekarang ada dua grup transisi. Satu untuk header, dan satu lagi untuk sisanya. Ini bisa ditargetkan secara independen dengan CSS, dan diberi transisi yang berbeda. Meskipun dalam kasus ini, main-header dibiarkan dengan transisi default, yaitu cross-fade.

Oke, transisi default tidak hanya memudar silang, ::view-transition-group juga bertransisi:

  • Posisi dan transformasi (melalui transform)
  • Lebar
  • Tinggi badan

Hal itu tidak menjadi masalah sampai sekarang, karena header memiliki ukuran yang sama dan memosisikan kedua sisi perubahan DOM. Tapi kita juga bisa mengekstrak teks di {i>header<i}:

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

fit-content digunakan sehingga elemen sesuai dengan ukuran teks, bukan membentang ke lebar yang tersisa. Tanpa ini, panah kembali mengurangi ukuran elemen teks {i>header<i}, sedangkan kita ingin agar memiliki ukuran yang sama di kedua laman.

Jadi sekarang kita memiliki tiga bagian untuk dimainkan:

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

Sekali lagi, gunakan saja {i>default-<i}nya:

Teks header geser. Demo minimal. Sumber.

Sekarang teks judul melakukan geseran kecil yang memuaskan untuk memberi ruang bagi tombol kembali.

Transisi proses debug

Karena Transisi Tampilan dibuat di atas animasi CSS, panel Animasi di Chrome DevTools sangat bagus untuk men-debug transisi.

Dengan menggunakan panel Animasi, Anda dapat menjeda animasi berikutnya, lalu menggeser bolak-balik melalui animasi. Selama proses ini, elemen pseudo transisi dapat ditemukan di panel Elements.

Men-debug Transisi Tampilan dengan Chrome Dev Tools.

Mentransisi elemen tidak harus berupa elemen DOM yang sama

Sejauh ini kita telah menggunakan view-transition-name untuk membuat elemen transisi terpisah untuk header dan teks di header. Ini adalah elemen yang sama secara konseptual sebelum dan setelah perubahan DOM, namun Anda dapat membuat transisi yang tidak demikian.

Misalnya, sematan video utama dapat diberi view-transition-name:

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

Kemudian, saat diklik, thumbnail dapat diberi view-transition-name yang sama, hanya selama durasi transisi:

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

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

Dan hasilnya:

Satu elemen bertransisi ke elemen lainnya. Demo minimal. Sumber.

Thumbnail kini bertransisi ke gambar utama. Meskipun keduanya merupakan elemen yang berbeda secara konseptual (dan secara harfiah) berbeda, API transisi memperlakukannya sebagai hal yang sama karena menggunakan view-transition-name yang sama.

Kode sesungguhnya untuk hal ini sedikit lebih rumit daripada contoh sederhana di atas, karena kode ini juga menangani transisi kembali ke halaman thumbnail. Lihat sumber untuk implementasi lengkap.

Transisi masuk dan keluar kustom

Lihat contoh berikut:

Masuk dan keluar dari sidebar. Demo minimal. Sumber.

Sidebar adalah bagian dari transisi:

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

Namun, tidak seperti header di contoh sebelumnya, sidebar tidak muncul di semua halaman. Jika kedua status memiliki sidebar, elemen pseudo transisi akan terlihat seperti ini:

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

Namun, jika sidebar hanya ada di halaman baru, elemen pseudo ::view-transition-old(sidebar) tidak akan ada di sana. Karena tidak ada gambar 'lama' untuk sidebar, pasangan gambar hanya akan memiliki ::view-transition-new(sidebar). Demikian pula, jika sidebar hanya ada di halaman lama, pasangan gambar hanya akan memiliki ::view-transition-old(sidebar).

Pada demo di atas, transisi sidebar secara berbeda bergantung pada apakah masuk, keluar, atau ada di kedua status. Ia masuk dengan meluncur dari kanan dan memudar, keluar dengan meluncur ke kanan dan memudar, dan tetap pada tempatnya berada di kedua keadaan.

Untuk membuat transisi masuk dan keluar tertentu, Anda dapat menggunakan class semu :only-child untuk menargetkan elemen pseudo lama/baru saat elemen pseudo lama/baru adalah satu-satunya turunan dalam pasangan gambar:

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

Dalam hal ini, tidak ada transisi khusus saat sidebar ada di kedua status, karena default-nya sempurna.

Pembaruan DOM asinkron, dan menunggu konten

Callback yang diteruskan ke .startViewTransition() dapat menampilkan promise, yang memungkinkan pembaruan DOM asinkron, dan menunggu konten penting siap.

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

Transisi tidak akan dimulai sampai promise terpenuhi. Selama periode ini, halaman akan dibekukan, sehingga penundaan di sini harus seminimal mungkin. Secara khusus, pengambilan jaringan harus dilakukan sebelum memanggil .startViewTransition() saat halaman masih interaktif secara penuh, bukan melakukannya sebagai bagian dari callback .startViewTransition().

Jika Anda memutuskan untuk menunggu gambar atau font siap, pastikan untuk menggunakan waktu tunggu yang agresif:

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

Namun, dalam beberapa kasus, lebih baik menghindari penundaan sama sekali, dan menggunakan konten yang sudah Anda miliki.

Mengoptimalkan konten yang sudah Anda miliki

Jika thumbnail bertransisi ke gambar yang lebih besar:

Transisi default adalah cross-fade, yang berarti thumbnail dapat memudar silang dengan gambar penuh yang belum dimuat.

Salah satu cara untuk menangani hal ini adalah dengan menunggu gambar penuh dimuat sebelum memulai transisi. Idealnya, hal ini dilakukan sebelum memanggil .startViewTransition(), sehingga halaman tetap interaktif, dan indikator lingkaran berputar dapat ditampilkan untuk menunjukkan kepada pengguna bahwa halaman sedang dimuat. Namun, dalam kasus ini ada cara yang lebih baik:

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

Thumbnail ini tidak akan memudar, hanya berada di bawah gambar penuh. Artinya, jika tampilan baru belum dimuat, thumbnail akan terlihat selama transisi. Artinya, transisi dapat langsung dimulai dan gambar penuh dapat dimuat sesuai waktunya.

Ini tidak akan berhasil jika tampilan baru menampilkan transparansi, tetapi dalam kasus ini kita tahu tidak, jadi kita dapat melakukan pengoptimalan ini.

Menangani perubahan dalam rasio aspek

Mudahnya, semua transisi sejauh ini dilakukan ke elemen dengan rasio aspek yang sama, tetapi hal itu tidak akan selalu berlaku. Bagaimana jika thumbnail adalah 1:1, dan gambar utama adalah 16:9?

Satu elemen bertransisi ke elemen lainnya, dengan perubahan rasio aspek. Demo minimal. Sumber.

Dalam transisi default, grup akan menganimasikan dari ukuran sebelum ke ukuran setelahnya. Tampilan lama dan baru memiliki lebar 100% dari grup, dan tinggi otomatis, yang berarti tampilan tersebut mempertahankan rasio aspeknya terlepas dari ukuran grup.

Ini adalah {i>default<i} yang baik, tetapi itu bukan yang kita inginkan dalam kasus ini. Jadi:

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

Ini berarti thumbnail tetap berada di tengah elemen saat lebar meluas, tetapi gambar penuh 'un-crops' saat bertransisi dari 1:1 ke 16:9.

Mengubah transisi bergantung pada status perangkat

Anda mungkin ingin menggunakan transisi yang berbeda di perangkat seluler vs. desktop, seperti contoh ini yang menampilkan slide lengkap dari samping di perangkat seluler, tetapi slide yang lebih samar di desktop:

Satu elemen bertransisi ke elemen lainnya. Demo minimal. Sumber.

Hal ini dapat dilakukan menggunakan kueri media biasa:

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

Anda mungkin juga ingin mengubah elemen mana yang Anda tetapkan untuk view-transition-name bergantung pada kueri media yang cocok.

Bereaksi terhadap preferensi 'gerakan dikurangi'

Pengguna dapat menunjukkan bahwa mereka lebih memilih gerakan yang dikurangi melalui sistem operasi mereka, dan preferensi tersebut ditampilkan melalui CSS.

Anda dapat memilih untuk mencegah transisi apa pun untuk pengguna ini:

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Namun, preferensi untuk 'pengurangan gerakan' tidak berarti pengguna menginginkan tidak ada gerakan. Alih-alih di atas, Anda dapat memilih animasi yang lebih halus, tetapi masih mengekspresikan hubungan antara elemen, dan aliran data.

Mengubah transisi bergantung pada jenis navigasi

Terkadang navigasi dari satu jenis halaman tertentu ke jenis halaman lainnya harus memiliki transisi yang disesuaikan secara khusus. Atau, navigasi 'kembali' harus berbeda dengan navigasi 'maju'.

Transisi yang berbeda saat kembali'. Demo minimal. Sumber.

Cara terbaik untuk menangani kasus ini adalah dengan menetapkan nama class di <html>, yang juga dikenal sebagai elemen dokumen:

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

Contoh ini menggunakan transition.finished, promise yang diselesaikan setelah transisi mencapai status akhirnya. Properti lain dari objek ini tercakup dalam referensi API.

Sekarang Anda dapat menggunakan nama class tersebut di CSS untuk mengubah transisi:

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

Seperti kueri media, keberadaan class ini juga dapat digunakan untuk mengubah elemen mana yang mendapatkan view-transition-name.

Melakukan transisi tanpa membekukan animasi lainnya

Lihat demo posisi transisi video berikut:

Transisi video. Demo minimal. Sumber.

Apakah Anda melihat ada yang salah dengannya? Jangan khawatir jika Anda tidak memilikinya. Di sini fungsi ini langsung diperlambat:

Transisi video, lebih lambat. Demo minimal. Sumber.

Selama transisi, video tampak berhenti berfungsi, lalu versi video yang diputar akan memudar. Hal ini karena ::view-transition-old(video) merupakan screenshot tampilan lama, sedangkan ::view-transition-new(video) adalah gambar aktif dari tampilan baru.

Anda dapat memperbaikinya, namun pertama-tama, tanyakan pada diri Anda sendiri apakah masalah ini perlu diperbaiki. Jika Anda tidak melihat 'masalah' saat transisi diputar dengan kecepatan normal, saya tidak akan repot-repot mengubahnya.

Jika Anda benar-benar ingin memperbaikinya, jangan tampilkan ::view-transition-old(video); beralihlah langsung ke ::view-transition-new(video). Anda bisa melakukannya dengan mengganti gaya dan animasi default:

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

Dan selesai!

Transisi video, lebih lambat. Demo minimal. Sumber.

Sekarang video dapat diputar selama transisi.

Menganimasikan dengan JavaScript

Sejauh ini, semua transisi telah ditentukan menggunakan CSS, tetapi terkadang CSS tidak cukup:

Transisi lingkaran. Demo minimal. Sumber.

Beberapa bagian dari transisi ini tidak dapat dilakukan hanya dengan CSS:

  • Animasi dimulai dari lokasi klik.
  • Animasi berakhir dengan lingkaran yang memiliki radius ke sudut terjauh. Namun, semoga hal ini dapat dilakukan dengan CSS di masa mendatang.

Untungnya, Anda dapat membuat transisi menggunakan 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)',
      }
    );
  });
}

Contoh ini menggunakan transition.ready, promise yang di-resolve setelah elemen pseudo transisi berhasil dibuat. Properti lain dari objek ini tercakup dalam referensi API.

Transisi sebagai peningkatan

View Transition API didesain untuk 'menggabungkan' perubahan DOM dan membuat transisi untuknya. Namun, transisi harus diperlakukan sebagai peningkatan, seperti dalam, aplikasi Anda tidak boleh memasuki status 'error' jika perubahan DOM berhasil, tetapi transisi gagal. Idealnya, transisi tidak boleh gagal, tetapi jika gagal, transisi tersebut tidak boleh mengganggu pengalaman pengguna lainnya.

Untuk memperlakukan transisi sebagai peningkatan, berhati-hatilah untuk tidak menggunakan promise transisi dengan cara yang akan menyebabkan aplikasi Anda ditampilkan jika transisi gagal.

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

Masalah dengan contoh ini adalah switchView() akan menolak jika transisi tidak dapat mencapai status ready, tetapi itu tidak berarti tampilan gagal dialihkan. DOM mungkin berhasil diperbarui, tetapi ada view-transition-name duplikat, sehingga transisi dilewati.

Sebagai gantinya:

Anjuran
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
  }
}

Contoh ini menggunakan transition.updateCallbackDone untuk menunggu update DOM, dan untuk menolaknya jika gagal. switchView tidak lagi menolak jika transisi gagal, akan diselesaikan saat update DOM selesai, dan menolak jika gagal.

Jika Anda ingin switchView menyelesaikan saat tampilan baru telah 'selesai', seperti pada, transisi animasi telah selesai atau dilewati sampai akhir, ganti transition.updateCallbackDone dengan transition.finished.

Bukan polyfill, tetapi...

Saya rasa fitur ini tidak dapat di-polyfill dengan cara apa pun yang berguna, tetapi saya senang terbukti salah!

Namun, fungsi bantuan ini membuat segalanya menjadi lebih mudah di browser yang tidak mendukung transisi tampilan:

function transitionHelper({
  skipTransition = false,
  classNames = [],
  updateDOM,
}) {
  if (skipTransition || !document.startViewTransition) {
    const updateCallbackDone = Promise.resolve(updateDOM()).then(() => {});

    return {
      ready: Promise.reject(Error('View transitions unsupported')),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
    };
  }

  document.documentElement.classList.add(...classNames);

  const transition = document.startViewTransition(updateDOM);

  transition.finished.finally(() =>
    document.documentElement.classList.remove(...classNames)
  );

  return transition;
}

Dan dapat digunakan seperti ini:

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

  const transition = transitionHelper({
    classNames,
    updateDOM() {
      updateTheDOMSomehow(data);
    },
  });

  // …
}

Di browser yang tidak mendukung Transisi Tampilan, updateDOM akan tetap dipanggil, tetapi tidak akan ada transisi animasi.

Anda juga dapat memberikan beberapa classNames untuk ditambahkan ke <html> selama transisi, sehingga mempermudah mengubah transisi bergantung pada jenis navigasi.

Anda juga dapat meneruskan true ke skipTransition jika tidak menginginkan animasi, bahkan di browser yang mendukung Transisi Tampilan. Hal ini berguna jika situs Anda memiliki preferensi pengguna untuk menonaktifkan transisi.

Bekerja dengan framework

Jika Anda bekerja dengan library atau framework yang mengabstraksikan perubahan DOM, bagian yang sulit adalah mengetahui kapan perubahan DOM selesai. Berikut adalah beberapa contoh, yang menggunakan helper di atas dalam berbagai framework.

  • React—kuncinya di sini adalah flushSync, yang menerapkan serangkaian perubahan status secara sinkron. Ya, ada peringatan besar tentang penggunaan API tersebut, tetapi Dan Abramov meyakinkan saya bahwa API tersebut sesuai dalam kasus ini. Seperti biasa dengan kode React dan asinkron, saat menggunakan berbagai promise yang ditampilkan oleh startViewTransition, pastikan kode Anda berjalan dengan status yang benar.
  • Vue.js—kuncinya di sini adalah nextTick, yang terpenuhi setelah DOM diperbarui.
  • Svelte—sangat mirip dengan Vue, tetapi metode untuk menunggu perubahan berikutnya adalah tick.
  • Lit—kuncinya di sini adalah promise this.updateComplete dalam komponen, yang terpenuhi setelah DOM diupdate.
  • Angular—kuncinya di sini adalah applicationRef.tick, yang menghapus perubahan DOM yang tertunda. Mulai Angular versi 17, Anda dapat menggunakan withViewTransitions yang disertakan dengan @angular/router.

Referensi API

const viewTransition = document.startViewTransition(updateCallback)

Mulai ViewTransition baru.

updateCallback dipanggil setelah status dokumen saat ini diambil.

Kemudian, setelah promise yang ditampilkan oleh updateCallback terpenuhi, transisi akan dimulai di frame berikutnya. Jika promise yang ditampilkan oleh updateCallback ditolak, transisi akan diabaikan.

Anggota instance ViewTransition:

viewTransition.updateCallbackDone

Promise yang terpenuhi jika promise yang ditampilkan oleh updateCallback terpenuhi, atau ditolak jika ditolak.

View Transition API menggabungkan perubahan DOM dan membuat transisi. Namun, terkadang Anda tidak peduli dengan keberhasilan/kegagalan animasi transisi, Anda hanya ingin tahu apakah dan kapan perubahan DOM terjadi. updateCallbackDone ditujukan untuk kasus penggunaan tersebut.

viewTransition.ready

Promise yang terpenuhi setelah elemen pseudo untuk transisi dibuat, dan animasi akan dimulai.

Tombol akan ditolak jika transisi tidak dapat dimulai. Hal ini dapat disebabkan oleh kesalahan konfigurasi, seperti view-transition-name duplikat, atau jika updateCallback menampilkan promise yang ditolak.

Hal ini berguna untuk menganimasikan elemen pseudo transisi dengan JavaScript.

viewTransition.finished

Promise yang terpenuhi setelah status akhir sepenuhnya terlihat dan interaktif bagi pengguna.

Fungsi ini hanya menolak jika updateCallback menampilkan promise yang ditolak, karena hal ini menunjukkan status akhir tidak dibuat.

Atau, jika transisi gagal dimulai, atau dilewati selama transisi, status akhir masih akan tercapai, sehingga finished akan terpenuhi.

viewTransition.skipTransition()

Lewati bagian animasi dari transisi.

Tindakan ini tidak akan melewatkan pemanggilan updateCallback, karena perubahan DOM terpisah dari transisi.

Referensi gaya dan transisi default

::view-transition
Elemen pseudo root yang mengisi area tampilan dan berisi setiap ::view-transition-group.
::view-transition-group

Sangat pas.

Transisi width dan height antara status 'sebelum' dan 'setelah'.

Mentransisi transform antara segi empat ruang pandang 'sebelum' dan 'setelah'.

::view-transition-image-pair

Berada di posisi yang benar-benar tepat untuk mengisi grup.

Memiliki isolation: isolate untuk membatasi efek mode campuran plus-lighter pada tampilan lama dan baru.

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

Posisinya benar-benar tepat di kiri atas wrapper.

Mengisi 100% lebar grup, tetapi memiliki tinggi otomatis, sehingga rasio aspeknya akan dipertahankan, bukan memenuhi grup.

Memiliki mix-blend-mode: plus-lighter untuk memungkinkan cross-fade yang sebenarnya.

Tampilan lama bertransisi dari opacity: 1 ke opacity: 0. Transisi tampilan baru dari opacity: 0 ke opacity: 1.

Masukan

Masukan developer sangat penting pada tahap ini, jadi laporkan masalah di GitHub dengan saran dan pertanyaan.