Salah satu situs klien yang saya kerjakan punya satu momen andalan di homepage: sederet tile sektor yang tersusun rapat sebagai strip, lalu begitu di-scroll, mereka morph jadi kartu fitur yang lebih besar. Animasinya saya bangun pakai GSAP Flip, karena Flip memang jago mengukur posisi awal dan akhir sebuah elemen lalu men-tween-nya sendiri tanpa saya perlu menghitung koordinat manual. Durasi 2 detik, ease expo.inOut, stagger 0.05. Di kepala saya mulus. Di layar, kacau.
Begitu morph mulai jalan, tinggi grid-nya tiba-tiba kolaps jadi nyaris nol, dan tile-tile yang sedang beranimasi malah numpuk menimpa section About tepat di bawahnya. Jadi bukan sekadar jelek: seluruh alur halaman ikut rusak. Konten setelah grid tersedot naik selama dua detik penuh, lalu balik normal persis saat animasi selesai. Balik normalnya pas di durasi animasi. Itu petunjuk pertama yang harusnya saya baca lebih cepat.
Menelusuri jejaknya
Refleks pertama saya salah sasaran: saya kira ini murni soal CSS Grid. Saya utak-atik min-height di container, saya set grid-auto-rows, saya coba kunci tinggi grid dengan aspect-ratio. Semua percuma. Tinggi grid tetap ambruk begitu morph mulai, dan begitu selesai, semuanya kembali seperti tidak terjadi apa-apa.
Karena masalahnya hidup dan mati persis mengikuti timeline animasi, saya berhenti menyalahkan CSS statis dan mulai mengintip DOM saat morph sedang berjalan. Saya taruh log kecil untuk membaca tinggi grid di tengah animasi:
Flip.from(state, {
duration: 2,
onUpdate: () => {
console.log(grid.offsetHeight, getComputedStyle(grid).position);
},
});Hasilnya menohok: selama dua detik itu, grid.offsetHeight turun ke angka kecil, dan tile-tile yang sedang morph punya position: absolute yang bukan saya tulis di CSS. Saya tidak pernah menaruh position: absolute di situ. Berarti ada yang menyuntikkannya saat runtime. Dan cuma ada satu tersangka yang menyentuh posisi elemen di sini: Flip sendiri.
Akar masalahnya
Biang keroknya satu opsi kecil di konfigurasi Flip yang saya contek mentah-mentah dari sebuah situs referensi desain:
// Versi yang bikin grid kolaps
const state = Flip.getState(".morph-tile");
grid.classList.add("is-feature"); // strip -> feature layout
Flip.from(state, {
duration: 2,
ease: "expo.inOut",
stagger: 0.05,
absolute: true, // <- ini biang keroknya
});absolute: true menyuruh Flip memposisikan elemen secara absolut selama animasi. Idenya sebenarnya masuk akal: dengan absolute, tile yang bergerak tidak lagi mendorong-dorong saudaranya, jadi tween-nya bisa mulus tanpa layout ikut bergoyang di setiap frame. Tapi ada konsekuensi yang tidak saya sadari. Elemen absolute keluar dari document flow. Selama dua detik itu, grid saya kehilangan SEMUA anak yang menyumbang tinggi, karena semuanya sedang melayang di luar flow. Grid tanpa anak in-flow otomatis kolaps ke tinggi nol. Dan karena tile-nya kini absolute relatif terhadap konteks di atasnya, mereka meluber turun dan menimpa section berikutnya. Dua gejala yang tadinya kelihatan terpisah ternyata satu akar: tinggi kolaps dan tile numpuk adalah dua sisi dari benda yang sama.
Lalu kenapa situs referensi bisa aman dengan absolute: true? Saya bongkar markup-nya, dan jawabannya: mereka me-re-parent elemen yang di-flip ke dalam sebuah container khusus yang tingginya sudah dipesan lebih dulu. Karena container itu punya tinggi sendiri, tile yang keluar dari flow grid tidak bikin apa-apa kolaps. Saya menyalin opsi-nya, tapi tidak menyalin arsitektur penampungnya. Itulah kenapa jalan di sana, jebol di saya.
Perbaikannya
Solusinya bukan menambal tinggi grid dengan angka ajaib, tapi menjalankan Flip tetap di dalam flow. Buang absolute: true. Selama tile tetap in-flow, grid tidak pernah kehilangan tinggi, jadi tidak ada yang perlu dipaksa lewat min-height:
// Bersihkan Flip yang mungkin masih jalan supaya getState
// membaca posisi final, bukan posisi tengah animasi sebelumnya
Flip.killFlipsOf(".morph-tile");
const state = Flip.getState(".morph-tile");
grid.classList.add("is-feature");
Flip.from(state, {
duration: 2, // sama persis dengan acuan
ease: "expo.inOut", // sama persis
stagger: 0.05, // sama persis
// tanpa absolute:true -> tile tetap di flow, tinggi grid aman
onComplete: () => ScrollTrigger.refresh(),
});Dua baris tambahan itu penting. Flip.killFlipsOf(".morph-tile") sebelum getState memastikan kalau user scroll bolak-balik cepat, Flip tidak membaca state dari tile yang masih di tengah tween lama, yang bikin posisi awalnya salah dan animasinya melompat. Dan ScrollTrigger.refresh() di onComplete menyuruh ScrollTrigger menghitung ulang semua posisi trigger, karena tinggi halaman berubah setelah layout strip berganti jadi feature. Tanpa refresh, trigger di bawah grid bergeser dan animasi berikutnya nyala di titik yang keliru.
Yang saya pertahankan verbatim: durasi, ease, dan stagger tetap sama seperti acuan. Yang saya buang cuma satu opsi yang tidak cocok dengan struktur DOM saya. Setelah itu morph mulus, grid tidak pernah kolaps, dan batas antara grid dengan section di bawahnya bersih dari frame pertama sampai terakhir.
Pelajaran
- Kalau layout kolaps persis selama durasi animasi lalu balik normal, tersangkanya animasi, bukan CSS statis kamu.
absolute: truedi GSAP Flip mengeluarkan elemen dari flow. Container yang isinya sedang di-flip bisa kehilangan tinggi dan kolaps ke nol.- Baca DOM saat animasi berjalan lewat
onUpdate, bukan cuma di state diam.position: absoluteyang tidak kamu tulis sendiri adalah petunjuk besar. - Menyalin opsi animasi dari situs lain tanpa menyalin arsitektur penampungnya adalah jebakan. Opsi yang sama butuh struktur DOM yang sama untuk berperilaku sama.
- Jalankan Flip in-flow kalau bisa. Panggil
Flip.killFlipsOf()sebelumgetState, danScrollTrigger.refresh()dionCompletesupaya trigger di bawah ikut menyesuaikan tinggi baru.
