Dipublikasikan: 17 Agustus 2021, Terakhir diperbarui: 25 September 2024
Jika transisi tampilan berjalan di satu dokumen, transisi tersebut disebut transisi tampilan dokumen yang sama. Hal ini biasanya terjadi di aplikasi web satu halaman (SPA) tempat JavaScript digunakan untuk mengupdate DOM. Transisi tampilan dokumen yang sama didukung di Chrome mulai Chrome 111.
Untuk memicu transisi tampilan dokumen yang sama, panggil 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());
}
Saat dipanggil, browser akan otomatis mengambil snapshot semua elemen yang memiliki properti CSS view-transition-name
yang dideklarasikan di dalamnya.
Kemudian, kode ini akan mengeksekusi callback yang diteruskan yang mengupdate DOM, lalu mengambil snapshot status baru.
Snapshot ini kemudian disusun dalam hierarki pseudo-elemen dan dianimasikan menggunakan kecanggihan animasi CSS. Pasangan snapshot dari status lama dan baru akan bertransisi dengan lancar dari posisi dan ukuran lama ke lokasi baru, sementara kontennya akan di-crossfade. Jika mau, Anda dapat menggunakan CSS untuk menyesuaikan animasi.
Transisi default: 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));
}
Di mana updateTheDOMSomehow
mengubah DOM ke status baru. Hal itu dapat dilakukan sesuai keinginan Anda. Misalnya, Anda dapat menambahkan atau menghapus elemen, mengubah nama class, atau mengubah gaya.
Dan seperti itu saja, halaman akan di-cross-fade:
Oke, transisi silang tidak terlalu mengesankan. Untungnya, transisi dapat disesuaikan, tetapi pertama-tama, Anda perlu memahami cara kerja cross-fade dasar ini.
Cara kerja transisi ini
Mari perbarui contoh kode sebelumnya.
document.startViewTransition(() => updateTheDOMSomehow(data));
Saat .startViewTransition()
dipanggil, API akan mengambil status halaman saat ini. Hal ini termasuk mengambil snapshot.
Setelah selesai, callback yang diteruskan ke .startViewTransition()
akan dipanggil. Di sinilah DOM diubah. Kemudian, API akan mengambil status baru halaman.
Setelah status baru diambil, API akan 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 elemen lain di halaman. Hal ini berguna jika Anda ingin menetapkan warna latar belakang untuk transisi.
::view-transition-old(root)
adalah screenshot tampilan lama, dan ::view-transition-new(root)
adalah representasi live dari tampilan baru. Keduanya dirender sebagai 'konten yang diganti' CSS (seperti <img>
).
Tampilan lama dianimasikan dari opacity: 1
ke opacity: 0
, sedangkan tampilan baru dianimasikan dari opacity: 0
ke opacity: 1
, sehingga membuat cross-fade.
Semua animasi dilakukan menggunakan animasi CSS, sehingga dapat disesuaikan dengan CSS.
Menyesuaikan transisi
Semua elemen pseudo transisi tampilan dapat ditargetkan dengan CSS, dan karena animasi ditentukan menggunakan CSS, Anda dapat mengubahnya menggunakan properti animasi CSS yang ada. Contoh:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 5s;
}
Dengan satu perubahan tersebut, proses memudar kini sangat lambat:
Oke, itu masih tidak mengesankan. Sebagai gantinya, kode berikut mengimplementasikan 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 berikut hasilnya:
Mentransisikan beberapa elemen
Dalam demo sebelumnya, seluruh halaman terlibat dalam transisi sumbu bersama. Hal ini berfungsi untuk sebagian besar halaman, tetapi sepertinya tidak tepat untuk judul, karena judul akan bergeser keluar hanya untuk bergeser kembali.
Untuk menghindari hal ini, Anda dapat mengekstrak header dari bagian halaman lainnya 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 none
, yang berarti tidak ada nama transisi). ID ini digunakan untuk mengidentifikasi elemen secara unik di seluruh transisi.
Dan hasilnya:
Sekarang header tetap berada di tempat dan melakukan cross-fade.
Deklarasi CSS tersebut menyebabkan hierarki elemen semu 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 bagian lainnya. Elemen ini dapat ditargetkan secara independen dengan CSS, dan diberi transisi yang berbeda. Meskipun demikian, dalam hal ini main-header
dibiarkan dengan transisi default, yaitu cross-fade.
Baiklah, transisi default bukan hanya cross fade, ::view-transition-group
juga bertransisi:
- Posisi dan transformasi (menggunakan
transform
) - Lebar
- Tinggi
Hal itu tidak penting hingga saat ini, karena header memiliki ukuran dan posisi yang sama di kedua sisi perubahan DOM. Namun, Anda juga dapat mengekstrak teks di header:
.main-header-text {
view-transition-name: main-header-text;
width: fit-content;
}
fit-content
digunakan agar elemen memiliki ukuran teks, bukan meluas ke lebar yang tersisa. Tanpa ini, panah kembali akan mengurangi ukuran elemen teks header, bukan ukuran yang sama di kedua halaman.
Jadi, sekarang kita memiliki tiga bagian untuk digunakan:
::view-transition
├─ ::view-transition-group(root)
│ └─ …
├─ ::view-transition-group(main-header)
│ └─ …
└─ ::view-transition-group(main-header-text)
└─ …
Namun, sekali lagi, kita akan menggunakan setelan default:
Sekarang teks judul bergeser sedikit untuk memberi ruang bagi tombol kembali.
Menganimasikan beberapa pseudo-elemen dengan cara yang sama dengan view-transition-class
Dukungan Browser
Misalnya, Anda memiliki transisi tampilan dengan sekumpulan kartu, tetapi juga memiliki judul di halaman. Untuk menganimasikan semua kartu kecuali judul, Anda harus menulis pemilih yang menargetkan setiap kartu.
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);
}
Punya 20 elemen? Artinya, ada 20 pemilih yang perlu Anda tulis. Menambahkan elemen baru? Kemudian, Anda juga perlu memperluas pemilih yang menerapkan gaya animasi. Tidak sepenuhnya skalabel.
view-transition-class
dapat digunakan dalam pseudo-elemen transisi tampilan untuk menerapkan aturan gaya yang sama.
#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);
}
Contoh kartu berikut memanfaatkan cuplikan CSS sebelumnya. Semua kartu–termasuk yang baru ditambahkan–mendapatkan pengaturan waktu yang sama dengan satu pemilih: html::view-transition-group(.card)
.
Men-debug transisi
Karena transisi tampilan dibuat di atas animasi CSS, panel Animations di Chrome DevTools sangat cocok untuk men-debug transisi.
Dengan menggunakan panel Animations, Anda dapat menjeda animasi berikutnya, lalu menggeser maju mundur melalui animasi. Selama ini, pseudo-elemen transisi dapat ditemukan di panel Elemen.
Elemen transisi 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. Secara konseptual, elemen ini sama sebelum dan sesudah perubahan DOM, tetapi Anda dapat membuat transisi jika tidak demikian.
Misalnya, penyematan 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 untuk durasi transisi:
thumbnail.onclick = async () => {
thumbnail.style.viewTransitionName = 'full-embed';
document.startViewTransition(() => {
thumbnail.style.viewTransitionName = '';
updateTheDOMSomehow();
});
};
Dan hasilnya:
Thumbnail kini bertransisi menjadi gambar utama. Meskipun secara konseptual (dan secara harfiah) merupakan elemen yang berbeda, API transisi memperlakukannya sebagai hal yang sama karena keduanya memiliki view-transition-name
yang sama.
Kode sebenarnya untuk transisi ini sedikit lebih rumit daripada contoh sebelumnya, karena juga menangani transisi kembali ke halaman thumbnail. Lihat sumber untuk implementasi lengkapnya.
Transisi masuk dan keluar kustom
Lihat contoh berikut:
Sidebar adalah bagian dari transisi:
.sidebar {
view-transition-name: sidebar;
}
Namun, tidak seperti header dalam contoh sebelumnya, sidebar tidak muncul di semua halaman. Jika kedua status memiliki sidebar, pseudo-elemen 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)
.
Dalam demo sebelumnya, transisi sidebar berbeda-beda, bergantung pada apakah sidebar memasuki, keluar, atau ada dalam kedua status. Menutup dengan menggeser dari kanan dan memudar, keluar dengan menggeser ke kanan dan memudar, dan tetap berada di tempat saat ada dalam kedua status.
Untuk membuat transisi masuk dan keluar tertentu, Anda dapat menggunakan class semu :only-child
untuk menargetkan elemen semu lama atau baru jika elemen tersebut 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 sudah sempurna.
Pembaruan DOM asinkron, dan menunggu konten
Callback yang diteruskan ke .startViewTransition()
dapat menampilkan promise, yang memungkinkan update DOM asinkron, dan menunggu konten penting siap.
document.startViewTransition(async () => {
await something;
await updateTheDOMSomehow();
await somethingElse;
});
Transisi tidak akan dimulai hingga promise terpenuhi. Selama waktu ini, halaman akan dibekukan, sehingga penundaan di sini harus diminimalkan. Secara khusus, pengambilan jaringan harus dilakukan sebelum memanggil .startViewTransition()
, saat halaman masih sepenuhnya interaktif, 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, sebaiknya hindari penundaan sama sekali, dan gunakan konten yang sudah Anda miliki.
Mengoptimalkan konten yang sudah Anda miliki
Jika thumbnail bertransisi ke gambar yang lebih besar:
Transisi default-nya adalah cross-fade, yang berarti thumbnail dapat melakukan cross-fade dengan gambar lengkap yang belum dimuat.
Salah satu cara untuk menangani hal ini adalah dengan menunggu gambar penuh dimuat sebelum memulai transisi. Idealnya, hal ini akan dilakukan sebelum memanggil .startViewTransition()
, sehingga halaman tetap interaktif, dan indikator lingkaran berputar dapat ditampilkan untuk menunjukkan kepada pengguna bahwa sesuatu sedang dimuat. Namun, dalam hal 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;
}
Sekarang thumbnail tidak memudar, tetapi hanya berada di bawah gambar lengkap. Artinya, jika tampilan baru belum dimuat, thumbnail akan terlihat selama transisi. Artinya, transisi dapat langsung dimulai, dan gambar lengkap dapat dimuat pada waktunya sendiri.
Hal ini tidak akan berfungsi jika tampilan baru menampilkan transparansi, tetapi dalam hal ini kita tahu tidak, sehingga kita dapat melakukan pengoptimalan ini.
Menangani perubahan rasio aspek
Semua transisi sejauh ini telah dilakukan ke elemen dengan rasio aspek yang sama, tetapi hal ini tidak selalu terjadi. Bagaimana jika thumbnail berukuran 1:1, dan gambar utama berukuran 16:9?
Dalam transisi default, grup akan dianimasikan dari ukuran sebelum ke ukuran setelah. 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 setelan default yang baik, tetapi bukan yang diinginkan 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;
}
Artinya, thumbnail tetap berada di tengah elemen saat lebar diperluas, tetapi gambar penuh 'tidak dipangkas' saat bertransisi dari 1:1 menjadi 16:9.
Untuk informasi yang lebih mendetail, lihat Transisi tampilan: Menangani perubahan rasio aspek
Menggunakan kueri media untuk mengubah transisi untuk status perangkat yang berbeda
Anda mungkin ingin menggunakan transisi yang berbeda di perangkat seluler dan desktop, seperti contoh ini yang melakukan slide penuh dari samping di perangkat seluler, tetapi slide yang lebih halus di desktop:
Hal ini dapat dicapai menggunakan kueri media reguler:
/* 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 juga dapat mengubah elemen yang ditetapkan view-transition-name
, bergantung pada kueri media yang cocok.
Bereaksi terhadap preferensi 'pengurangan gerakan'
Pengguna dapat menunjukkan bahwa mereka lebih memilih gerakan yang dikurangi melalui sistem operasi mereka, dan preferensi tersebut ditampilkan di CSS.
Anda dapat memilih untuk mencegah transisi apa pun bagi pengguna ini:
@media (prefers-reduced-motion) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) {
animation: none !important;
}
}
Namun, preferensi untuk 'gerakan yang dikurangi' bukan berarti pengguna menginginkan tidak ada gerakan. Sebagai ganti cuplikan sebelumnya, Anda dapat memilih animasi yang lebih halus, tetapi tetap mengekspresikan hubungan antara elemen, dan alur data.
Menangani beberapa gaya transisi tampilan dengan jenis transisi tampilan
Dukungan Browser
Terkadang transisi dari satu tampilan tertentu ke tampilan lainnya harus memiliki transisi yang disesuaikan secara khusus. Misalnya, saat membuka halaman berikutnya atau sebelumnya dalam urutan penomoran halaman, Anda mungkin ingin menggeser konten ke arah yang berbeda, bergantung pada apakah Anda membuka halaman yang lebih tinggi atau lebih rendah dari urutan.
Untuk melakukannya, Anda dapat menggunakan jenis transisi tampilan, yang memungkinkan Anda menetapkan satu atau beberapa jenis ke transisi tampilan aktif. Misalnya, saat bertransisi ke halaman yang lebih tinggi dalam urutan penomoran halaman, gunakan jenis forwards
dan saat membuka halaman yang lebih rendah, gunakan jenis backwards
. Jenis ini hanya aktif saat mengambil atau melakukan transisi, dan setiap jenis dapat disesuaikan melalui CSS untuk menggunakan animasi yang berbeda.
Untuk menggunakan jenis dalam transisi tampilan dokumen yang sama, Anda meneruskan types
ke metode startViewTransition
. Untuk mengizinkan hal ini, document.startViewTransition
juga menerima objek: update
adalah fungsi callback yang memperbarui DOM, dan types
adalah array dengan jenis.
const direction = determineBackwardsOrForwards();
const t = document.startViewTransition({
update: updateTheDOMSomehow,
types: ['slide', direction],
});
Untuk merespons jenis ini, gunakan pemilih :active-view-transition-type()
. Teruskan type
yang ingin Anda targetkan ke pemilih. Hal ini memungkinkan Anda mempertahankan gaya beberapa transisi tampilan yang terpisah satu sama lain, tanpa deklarasi yang mengganggu deklarasi yang lain.
Karena jenis hanya berlaku saat mengambil atau melakukan transisi, Anda dapat menggunakan pemilih untuk menetapkan–atau membatalkan penetapan–view-transition-name
pada elemen hanya untuk transisi tampilan dengan jenis tersebut.
/* 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;
}
}
Dalam demo penomoran halaman berikut, konten halaman akan bergeser maju atau mundur berdasarkan nomor halaman yang Anda buka. Jenis ditentukan saat diklik, lalu diteruskan ke document.startViewTransition
.
Untuk menargetkan transisi tampilan aktif, apa pun jenisnya, Anda dapat menggunakan pemilih pseudo-class :active-view-transition
.
html:active-view-transition {
…
}
Menangani beberapa gaya transisi tampilan dengan nama class di root transisi tampilan
Terkadang transisi dari satu jenis tampilan tertentu ke jenis tampilan lainnya harus memiliki transisi yang disesuaikan secara khusus. Atau, navigasi 'kembali' harus berbeda dengan navigasi 'maju'.
Sebelum jenis transisi, cara menangani kasus ini adalah dengan menetapkan nama class di root transisi untuk sementara. Saat memanggil document.startViewTransition
, root transisi ini adalah elemen <html>
, yang dapat diakses menggunakan document.documentElement
di 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');
}
Untuk menghapus class setelah transisi selesai, 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, kehadiran class ini juga dapat digunakan untuk mengubah elemen mana yang mendapatkan view-transition-name
.
Menjalankan transisi tanpa membekukan animasi lain
Lihat demo posisi transisi video ini:
Apakah Anda melihat ada yang salah? Jangan khawatir jika Anda tidak melakukannya. Berikut adalah perlambatannya:
Selama transisi, video akan tampak berhenti, lalu versi video yang diputar akan muncul secara perlahan. Hal ini karena ::view-transition-old(video)
adalah screenshot tampilan lama, sedangkan ::view-transition-new(video)
adalah gambar live dari tampilan baru.
Anda dapat memperbaikinya, tetapi pertama-tama, tanyakan pada diri sendiri apakah error tersebut benar-benar perlu diperbaiki. Jika Anda tidak melihat 'masalah' saat transisi diputar dengan kecepatan normal, saya tidak akan mengubahnya.
Jika Anda benar-benar ingin memperbaikinya, jangan tampilkan ::view-transition-old(video)
; langsung beralih ke ::view-transition-new(video)
. Anda dapat 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;
}
Selesai.
Sekarang video akan diputar selama transisi.
Integrasi dengan Navigation API (dan framework lainnya)
Transisi tampilan ditentukan sedemikian rupa sehingga dapat diintegrasikan dengan framework atau library lain. Misalnya, jika aplikasi web satu halaman (SPA) menggunakan router, Anda dapat menyesuaikan mekanisme pembaruan router untuk memperbarui konten menggunakan transisi tampilan.
Dalam cuplikan kode berikut yang diambil dari demo penomoran halaman ini, pengelola intersepsi Navigation API disesuaikan untuk memanggil document.startViewTransition
saat transisi tampilan didukung.
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);
});
}
});
});
Beberapa, tetapi tidak semua, browser menyediakan transisi mereka sendiri saat pengguna melakukan gestur geser untuk menavigasi. Dalam hal ini, Anda tidak boleh memicu transisi tampilan Anda sendiri karena akan menyebabkan pengalaman pengguna yang buruk atau membingungkan. Pengguna akan melihat dua transisi—satu disediakan oleh browser dan satu lagi oleh Anda—yang berjalan secara berurutan.
Oleh karena itu, sebaiknya hindari transisi tampilan dimulai saat browser telah menyediakan transisi visualnya sendiri. Untuk melakukannya, periksa nilai properti hasUAVisualTransition
instance NavigateEvent
. Properti ditetapkan ke true
saat browser telah memberikan transisi visual. Properti hasUIVisualTransition
ini juga ada di instance PopStateEvent
.
Dalam cuplikan sebelumnya, pemeriksaan yang menentukan apakah akan menjalankan transisi tampilan mempertimbangkan properti ini. Jika tidak ada dukungan untuk transisi tampilan dokumen yang sama atau jika browser sudah menyediakan transisi sendiri, transisi tampilan akan dilewati.
if (!document.startViewTransition || e.hasUAVisualTransition) {
setContent(newContent);
return;
}
Dalam rekaman berikut, pengguna menggeser untuk kembali ke halaman sebelumnya. Rekaman di sebelah kiri tidak menyertakan pemeriksaan untuk tanda hasUAVisualTransition
. Rekaman di sebelah kanan menyertakan pemeriksaan, sehingga melewati transisi tampilan manual karena browser menyediakan transisi visual.
Menganimasikan dengan JavaScript
Sejauh ini, semua transisi telah ditentukan menggunakan CSS, tetapi terkadang CSS tidak cukup:
Beberapa bagian dari transisi ini tidak dapat dicapai dengan CSS saja:
- Animasi dimulai dari lokasi klik.
- Animasi berakhir dengan lingkaran yang memiliki jari-jari 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 pseudo-elemen transisi berhasil dibuat. Properti lain dari objek ini tercakup dalam referensi API.
Transisi sebagai peningkatan
View Transition API dirancang untuk 'menggabungkan' perubahan DOM dan membuat transisi untuknya. Namun, transisi harus diperlakukan sebagai peningkatan, seperti, aplikasi Anda tidak boleh memasuki status 'error' jika perubahan DOM berhasil, tetapi transisi gagal. Idealnya, transisi tidak boleh gagal, tetapi jika gagal, transisi tidak boleh merusak pengalaman pengguna lainnya.
Untuk memperlakukan transisi sebagai peningkatan, berhati-hatilah agar tidak menggunakan promise transisi dengan cara yang akan menyebabkan aplikasi Anda ditampilkan jika transisi gagal.
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 pada contoh ini adalah switchView()
akan ditolak jika transisi tidak dapat mencapai status ready
, tetapi hal itu tidak berarti tampilan gagal beralih. DOM mungkin berhasil diperbarui, tetapi ada view-transition-name
duplikat, sehingga transisi dilewati.
Sebagai gantinya:
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 menolak jika gagal. switchView
tidak lagi menolak jika transisi gagal, switchView
akan di-resolve saat update DOM selesai, dan menolak jika gagal.
Jika Anda ingin switchView
diselesaikan saat tampilan baru telah 'selesai', seperti, transisi animasi telah selesai atau dilewati hingga akhir, ganti transition.updateCallbackDone
dengan transition.finished
.
Bukan polyfill, tetapi…
Ini bukan fitur yang mudah untuk di-polyfill. Namun, fungsi bantuan ini mempermudah banyak hal di browser yang tidak mendukung transisi tampilan:
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');
}
}
Dan dapat digunakan seperti ini:
function spaNavigate(data) {
const types = isBackNavigation ? ['back-transition'] : [];
const transition = transitionHelper({
update() {
updateTheDOMSomehow(data);
},
types,
});
// …
}
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 perubahan 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.
Menggunakan framework
Jika Anda menggunakan library atau framework yang memisahkan perubahan DOM, bagian yang sulit adalah mengetahui kapan perubahan DOM selesai. Berikut adalah kumpulan contoh, menggunakan helper di atas, di 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 olehstartViewTransition
, 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 diperbarui. - Angular—kuncinya di sini adalah
applicationRef.tick
, yang menghapus perubahan DOM yang tertunda. Mulai Angular versi 17, Anda dapat menggunakanwithViewTransitions
yang disertakan dengan@angular/router
.
Referensi API
const viewTransition = document.startViewTransition(update)
Mulai
ViewTransition
baru.update
adalah fungsi yang dipanggil setelah status dokumen saat ini diambil.Kemudian, saat promise yang ditampilkan oleh
updateCallback
terpenuhi, transisi akan dimulai di frame berikutnya. Jika promise yang ditampilkan olehupdateCallback
ditolak, transisi akan ditinggalkan.const viewTransition = document.startViewTransition({ update, types })
Memulai
ViewTransition
baru dengan jenis yang ditentukanupdate
dipanggil setelah status dokumen saat ini diambil.types
menetapkan jenis aktif untuk transisi saat mengambil atau melakukan transisi. Awalnya kosong. LihatviewTransition.types
di bawah untuk mengetahui informasi selengkapnya.
Anggota instance ViewTransition
:
viewTransition.updateCallbackDone
Promise yang terpenuhi saat promise yang ditampilkan oleh
updateCallback
terpenuhi, atau ditolak saat ditolak.View Transition API menggabungkan perubahan DOM dan membuat transisi. Namun, terkadang Anda tidak peduli dengan keberhasilan atau kegagalan animasi transisi, Anda hanya ingin tahu apakah dan kapan perubahan DOM terjadi.
updateCallbackDone
adalah untuk kasus penggunaan tersebut.viewTransition.ready
Promise yang terpenuhi setelah elemen pseudo untuk transisi dibuat, dan animasi akan segera dimulai.
Tindakan ini akan ditolak jika transisi tidak dapat dimulai. Hal ini dapat disebabkan oleh kesalahan konfigurasi, seperti
view-transition-name
duplikat, atau jikaupdateCallback
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.
Hal ini hanya ditolak jika
updateCallback
menampilkan promise yang ditolak, karena hal ini menunjukkan bahwa status akhir tidak dibuat.Jika tidak, jika transisi gagal dimulai, atau dilewati selama transisi, status akhir masih tercapai, sehingga
finished
terpenuhi.viewTransition.types
Objek seperti
Set
yang menyimpan jenis transisi tampilan aktif. Untuk memanipulasi entri, gunakan metode instance-nyaclear()
,add()
, dandelete()
.Untuk merespons jenis tertentu di CSS, gunakan pemilih pseudo-class
:active-view-transition-type(type)
di root transisi.Jenis akan otomatis dibersihkan saat transisi tampilan selesai.
viewTransition.skipTransition()
Lewati bagian animasi transisi.
Tindakan ini tidak akan melewati panggilan
updateCallback
, karena perubahan DOM terpisah dari transisi.
Referensi gaya dan transisi default
::view-transition
- Pseudo-elemen root yang mengisi area pandang dan berisi setiap
::view-transition-group
. ::view-transition-group
Diposisikan secara mutlak.
Transisi
width
danheight
antara status 'sebelum' dan 'setelah'.Transisi
transform
antara quad ruang tampilan 'sebelum' dan 'setelah'.::view-transition-image-pair
Diposisikan secara mutlak untuk mengisi grup.
Memiliki
isolation: isolate
untuk membatasi efekmix-blend-mode
pada tampilan lama dan baru.::view-transition-new
dan::view-transition-old
Diposisikan secara mutlak ke kiri atas wrapper.
Mengisi 100% lebar grup, tetapi memiliki tinggi otomatis, sehingga akan mempertahankan rasio aspeknya, bukan mengisi grup.
Memiliki
mix-blend-mode: plus-lighter
untuk memungkinkan cross-fade yang sebenarnya.Tampilan lama bertransisi dari
opacity: 1
keopacity: 0
. Tampilan baru bertransisi dariopacity: 0
keopacity: 1
.
Masukan
Masukan developer selalu kami hargai. Untuk melakukannya, laporkan masalah ke CSS Working Group di GitHub dengan saran dan pertanyaan. Beri awalan [css-view-transitions]
pada masalah Anda.
Jika Anda mengalami bug, laporkan bug Chromium.