Jika transisi tampilan berjalan pada satu dokumen, transisi ini disebut transisi tampilan dokumen yang sama. Hal ini biasanya terjadi dalam aplikasi web satu halaman (SPA) yang menggunakan JavaScript untuk memperbarui 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 otomatis mengambil snapshot semua elemen yang memiliki properti CSS view-transition-name
yang dideklarasikan di dalamnya.
Selanjutnya, kode ini akan mengeksekusi callback yang diteruskan yang memperbarui DOM, setelah itu akan mengambil snapshot status baru.
Snapshot ini kemudian disusun dalam hierarki elemen pseudo dan dianimasikan menggunakan kekuatan animasi CSS. Sepasang snapshot dari status lama dan baru beralih dengan lancar dari posisi dan ukuran lama ke lokasi baru, sementara kontennya mengalami 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));
}
Tempat updateTheDOMSomehow
mengubah DOM ke status baru. Itu dapat dilakukan sesuai keinginan Anda. Misalnya, Anda dapat menambahkan atau menghapus elemen, mengubah nama kelas, atau mengubah gaya.
Dan begitulah, laman memudar bersilang:
Oke, cross-fade tidak terlalu mengesankan. Untungnya, transisi bisa disesuaikan, tapi pertama-tama, Anda perlu memahami cara kerja cross-fade dasar ini.
Cara kerja transisi ini
Mari kita update contoh kode sebelumnya.
document.startViewTransition(() => updateTheDOMSomehow(data));
Saat .startViewTransition()
dipanggil, API merekam status halaman saat ini. Termasuk mengambil {i>snapshot<i}.
Setelah selesai, callback yang diteruskan ke .startViewTransition()
akan dipanggil. Di situlah DOM diubah. Kemudian, API menangkap status baru halaman.
Setelah status baru didapatkan, API akan membuat hierarki elemen semu seperti ini:
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
::view-transition
ditempatkan 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 langsung dari tampilan baru tersebut. Keduanya merender sebagai CSS 'menggantikan konten' (seperti <img>
).
Tampilan lama dianimasikan dari opacity: 1
ke opacity: 0
, sedangkan tampilan baru dianimasikan dari opacity: 0
ke opacity: 1
, yang menghasilkan efek cross-fade.
Semua animasi dijalankan menggunakan animasi CSS, sehingga dapat disesuaikan dengan CSS.
Menyesuaikan transisi
Semua elemen semu transisi tampilan dapat ditargetkan dengan CSS, dan karena animasi ditentukan menggunakan CSS, Anda dapat mengubahnya menggunakan properti animasi CSS yang sudah ada. Contoh:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 5s;
}
Dengan satu perubahan tersebut, efek pudar sekarang menjadi sangat lambat:
Oke, itu masih belum mengesankan. Sebagai gantinya, kode berikut menerapkan 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:
Mentransisikan beberapa elemen
Pada demo sebelumnya, seluruh halaman terlibat dalam transisi sumbu merata. Cara ini berfungsi untuk sebagian besar halaman, tetapi tampaknya tidak tepat untuk judul, karena akan meluncur keluar hanya untuk meluncur kembali.
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). Fungsi ini digunakan untuk mengidentifikasi elemen di seluruh transisi secara unik.
Dan hasilnya:
Sekarang {i>header<i} 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 {i>header<i}, dan satu lagi untuk sisanya. Hal ini dapat ditargetkan secara independen dengan CSS, dan diberikan transisi yang berbeda. Meskipun demikian, dalam kasus ini, main-header
dibiarkan dengan transisi default, yang merupakan cross-fade.
Baik, transisi default bukan hanya cross fade, ::view-transition-group
juga bertransisi:
- Posisi dan transformasi (menggunakan
transform
) - Lebar
- Tinggi
Hal itu tidak penting sampai sekarang, karena header memiliki ukuran dan posisi yang sama di kedua sisi perubahan DOM. Tetapi Anda juga dapat mengekstrak teks di {i>header<i}:
.main-header-text {
view-transition-name: main-header-text;
width: fit-content;
}
fit-content
digunakan agar elemen sesuai dengan ukuran teks, bukan melebar ke lebar yang tersisa. Tanpanya, panah kembali akan mengurangi ukuran elemen teks {i>header<i}, bukan ukuran yang sama di kedua halaman.
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)
└─ …
Namun sekali lagi, gunakan nilai default:
Sekarang teks judul memiliki {i>slide<i} yang sedikit memuaskan untuk memberi ruang bagi tombol kembali.
Menganimasikan beberapa elemen pseudo dengan cara yang sama menggunakan view-transition-class
Dukungan Browser
- 125
- 125
- x
- x
Misalnya, Anda memiliki transisi tampilan dengan banyak kartu, tetapi juga judul pada 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? Itu 20 pemilih yang perlu Anda tulis. Menambahkan elemen baru? Kemudian Anda juga perlu meningkatkan pemilih yang menerapkan gaya animasi. Tidak benar-benar skalabel.
view-transition-class
dapat digunakan dalam elemen pseudo 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)
.
view-transition-class
digunakan, ini akan menerapkan animation-timing-function
yang sama untuk semua kartu, kecuali yang ditambahkan atau dihapus.Men-debug transisi
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 berpindah-pindah di antara animasi. Selama ini, elemen pseudo transisi dapat ditemukan di panel Elemen.
Melakukan transisi 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. Secara konseptual, ini adalah elemen yang sama sebelum dan sesudah perubahan DOM, tetapi Anda bisa membuat transisi yang sebenarnya tidak seperti itu.
Misalnya, sematan video utama dapat diberi view-transition-name
:
.full-embed {
view-transition-name: full-embed;
}
Kemudian, saat thumbnail diklik, thumbnail dapat diberikan 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:
Thumbnail kini bertransisi ke gambar utama. Meskipun secara konseptual (dan secara harfiah) berbeda, API transisi memperlakukannya sebagai hal yang sama karena menggunakan view-transition-name
yang sama.
Kode sebenarnya untuk transisi ini sedikit lebih rumit daripada contoh sebelumnya, karena kode ini juga menangani transisi kembali ke halaman thumbnail. Lihat sumbernya untuk implementasi lengkap.
Transisi masuk dan keluar kustom
Lihat contoh berikut:
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 semu ::view-transition-old(sidebar)
tidak akan ada di sana. Karena tidak ada kata 'lama' gambar 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, sidebar melakukan transisi secara berbeda, tergantung apakah sidebar masuk, keluar, atau ada dalam kedua status. Masuk dengan meluncur dari kanan dan memudar, keluar dengan meluncur ke kanan dan memudar, dan tetap di tempatnya saat ada di kedua keadaan.
Untuk membuat transisi masuk dan keluar yang spesifik, Anda dapat menggunakan class semu :only-child
untuk menargetkan elemen pseudo lama atau baru jika merupakan 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 kasus ini, tidak ada transisi khusus saat sidebar ada di kedua status, karena defaultnya adalah 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 sampai promise terpenuhi. Selama waktu ini, halaman akan dibekukan, sehingga penundaan di sini harus seminimal mungkin. 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, lebih baik hindari penundaan sama sekali, dan gunakan konten yang sudah Anda miliki.
Manfaatkan konten yang sudah Anda miliki
Jika thumbnail berubah ke gambar yang lebih besar:
Transisi defaultnya 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, ini dilakukan sebelum memanggil .startViewTransition()
, sehingga halaman tetap interaktif, dan indikator lingkaran berputar dapat ditampilkan untuk menunjukkan kepada pengguna bahwa ada sesuatu yang 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;
}
Kini thumbnail tidak 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 tepat waktu.
Ini tidak akan berfungsi jika tampilan baru menampilkan transparansi, tetapi dalam kasus ini kita tahu bahwa hal tersebut tidak berfungsi, jadi kita dapat melakukan pengoptimalan ini.
Menangani perubahan pada rasio aspek
Mudahnya, semua transisi sejauh ini dilakukan ke elemen dengan rasio aspek yang sama, tetapi tidak selalu demikian. Bagaimana jika thumbnail berukuran 1:1, dan gambar utamanya berukuran 16:9?
Dalam transisi default, grup dianimasikan dari ukuran sebelum ke ukuran sesudah. Tampilan lama dan baru memiliki lebar 100% grup, serta tinggi otomatis, artinya tampilan tersebut mempertahankan rasio aspeknya terlepas dari ukuran grup.
Ini adalah bawaan yang baik, tetapi bukan itu 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;
}
Ini berarti thumbnail tetap berada di tengah elemen saat lebarnya diperluas, tetapi gambar penuh 'un-crops' karena transisi dari 1:1 ke 16:9.
Untuk informasi selengkapnya, lihat Transisi tampilan: Menangani perubahan rasio aspek
Menggunakan kueri media untuk mengubah transisi untuk berbagai status perangkat
Anda mungkin ingin menggunakan transisi yang berbeda di perangkat seluler versus desktop, seperti contoh ini yang menampilkan slide penuh dari samping di perangkat seluler, tetapi slide yang lebih halus di desktop:
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 view-transition-name
bergantung pada kueri media yang cocok.
Bereaksi terhadap 'gerakan berkurang' preferensi
Pengguna dapat menunjukkan bahwa mereka lebih menyukai gerakan yang dikurangi melalui sistem operasi mereka, dan preferensi tersebut ditampilkan di CSS.
Anda dapat memilih untuk mencegah transisi 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 gerakan. Alih-alih cuplikan sebelumnya, Anda bisa memilih animasi yang lebih halus, tetapi animasi yang masih mengekspresikan hubungan antar elemen, dan aliran data.
Menangani beberapa gaya transisi tampilan dengan jenis transisi tampilan
Dukungan Browser
- 125
- 125
- x
- x
Terkadang, transisi dari satu tampilan tertentu ke tampilan lain memerlukan 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 akan membuka halaman yang lebih tinggi atau halaman yang lebih rendah dari urutan tersebut.
Untuk itu, Anda dapat menggunakan jenis transisi tampilan, yang memungkinkan Anda menetapkan satu atau beberapa jenis ke transisi tampilan aktif. Misalnya, saat beralih 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 merekam 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 harus meneruskan types
ke dalam metode startViewTransition
. Untuk mengizinkannya, 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 untuk menjaga gaya beberapa transisi tampilan tetap terpisah satu sama lain, tanpa deklarasi satu transisi mengganggu deklarasi yang lain.
Karena jenis hanya berlaku saat merekam atau melakukan transisi, Anda dapat menggunakan pemilih untuk menyetel–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;
}
}
Pada demo penomoran halaman berikut, konten halaman bergeser maju atau mundur berdasarkan nomor halaman yang Anda buka. Jenis ditentukan berdasarkan klik tempat jenis tersebut diteruskan ke document.startViewTransition
.
Untuk menargetkan transisi tampilan aktif, apa pun jenisnya, Anda dapat menggunakan pemilih class semu :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 memerlukan transisi yang disesuaikan secara khusus. Atau, 'kembali' navigasi harus berbeda dengan alur 'maju', navigasi.
Sebelum jenis transisi, cara untuk menangani kasus ini adalah dengan menetapkan nama class untuk sementara pada root transisi. 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 di-resolve setelah transisi mencapai status akhirnya. Properti lain dari objek ini dibahas 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
.
Menjalankan transisi tanpa membekukan animasi lain
Lihat demo posisi transisi video berikut:
Apakah Anda melihat ada yang salah dengan itu? Jangan khawatir jika Anda belum melakukannya. Di sini diperlambat langsung:
Selama transisi, video tampak berhenti berfungsi, lalu versi video yang diputar makin jelas. Hal ini dikarenakan ::view-transition-old(video)
adalah screenshot tampilan lama, sedangkan ::view-transition-new(video)
adalah gambar live dari tampilan baru tersebut.
Anda dapat memperbaikinya, tetapi pertama-tama, tanyakan pada diri Anda apakah hal ini layak diperbaiki. Jika Anda tidak melihat 'masalah' ketika transisi diputar dengan kecepatan normal, saya tidak perlu mengubahnya.
Jika Anda benar-benar ingin memperbaikinya, maka 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 diputar selama transisi.
Menganimasikan dengan JavaScript
Sejauh ini, semua transisi telah ditetapkan menggunakan CSS, tetapi terkadang CSS tidak cukup:
Beberapa bagian dari transisi ini tidak dapat dilakukan dengan CSS saja:
- Animasi dimulai dari lokasi klik.
- Animasi diakhiri dengan lingkaran yang memiliki radius ke sudut terjauh. Meskipun demikian, semoga hal ini dapat terjadi 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 semu transisi berhasil dibuat. Properti lain dari objek ini dibahas dalam referensi API.
Transisi sebagai peningkatan
View Transition API didesain untuk 'digabungkan' perubahan DOM dan membuat transisi untuknya. Namun, transisi harus diperlakukan sebagai peningkatan, seperti dalam, aplikasi Anda tidak boleh memasuki 'error' yang menyatakan jika perubahan DOM berhasil, tetapi transisi gagal. Idealnya, transisi tidak boleh gagal, tetapi jika hal itu terjadi, seharusnya tidak mengganggu pengalaman pengguna yang lain.
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 menolak jika transisi tidak dapat mencapai status ready
, tetapi bukan berarti tampilan gagal dialihkan. DOM mungkin telah berhasil diperbarui, tetapi terdapat 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 pembaruan DOM, dan menolak jika gagal. switchView
tidak lagi menolak jika transisi gagal, dan akan teratasi saat pembaruan DOM selesai, dan menolak jika gagal.
Jika Anda ingin switchView
me-resolve saat tampilan baru telah 'selesai', seperti dalam, transisi animasi telah selesai atau dilewati sampai akhir, ganti transition.updateCallbackDone
dengan transition.finished
.
Bukan polyfill, tetapi...
Ini bukan fitur yang mudah untuk di-polyfill. Namun, fungsi bantuan ini mempermudah browser 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 menyediakan 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, meskipun 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 pustaka atau kerangka kerja yang memisahkan perubahan DOM, bagian rumitnya adalah mengetahui kapan perubahan DOM selesai. Berikut ini kumpulan contoh, menggunakan helper di atas, dalam berbagai framework.
- Bereaksi—kuncinya di sini adalah
flushSync
, yang menerapkan kumpulan 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 akan 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 akan 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 diabaikan.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 merekam atau melakukan transisi. Kolom ini awalnya kosong. LihatviewTransition.types
di bagian bawah untuk informasi selengkapnya.
Anggota instance ViewTransition
:
viewTransition.updateCallbackDone
Promise yang terpenuhi saat promise yang ditampilkan oleh
updateCallback
terpenuhi, atau ditolak jika ditolak.View Transition API menggabungkan perubahan DOM dan membuat transisi. Namun, terkadang Anda tidak mementingkan keberhasilan atau kegagalan animasi transisi. Anda hanya ingin mengetahui apakah dan kapan perubahan DOM terjadi.
updateCallbackDone
adalah untuk kasus penggunaan tersebut.viewTransition.ready
Promise yang terpenuhi setelah elemen semu untuk transisi dibuat, dan animasi akan dimulai.
Kebijakan ini akan menolak jika transisi tidak dapat dimulai. Hal ini dapat disebabkan oleh kesalahan konfigurasi, seperti
view-transition-name
duplikat, atau jikaupdateCallback
menampilkan promise yang ditolak.Fungsi ini berguna untuk menganimasikan elemen pseudo transisi dengan JavaScript.
viewTransition.finished
Promise yang terpenuhi setelah status akhir sepenuhnya terlihat dan interaktif bagi pengguna.
Kode 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 tercapai, sehingga
finished
akan 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 class semu
:active-view-transition-type(type)
pada root transisi.Jenis akan otomatis dibersihkan saat transisi tampilan selesai.
viewTransition.skipTransition()
Lewati bagian animasi pada transisi.
Ini tidak akan melewati pemanggilan
updateCallback
, karena perubahan DOM terpisah dari transisi.
Referensi gaya dan transisi default
::view-transition
- Elemen semu root yang mengisi area tampilan dan berisi setiap
::view-transition-group
. ::view-transition-group
Posisinya mutlak.
Transisi
width
danheight
antara 'sebelum' dan 'setelah' negara bagian.Transisi
transform
antara 'sebelum' dan 'setelah' ruang pandang-ruang.::view-transition-image-pair
Posisinya benar-benar tepat untuk mengisi grup.
Memiliki
isolation: isolate
untuk membatasi efekmix-blend-mode
pada tampilan lama dan baru.::view-transition-new
dan::view-transition-old
Benar-benar diposisikan di 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, ajukan masalah ke Kelompok Kerja CSS di GitHub beserta saran dan pertanyaan. Awali masalah Anda dengan [css-view-transitions]
.
Jika Anda mengalami bug, laporkan bug Chromium.