D
P
0
← Semua artikel Read in English

JavaScript, DOM & Animasi Browser

Animasi Tutup Drawer Kepotong? `visibility: hidden` Balik Seketika Begitu Class Dilepas

· · 5 menit baca
Animasi Tutup Drawer Kepotong? `visibility: hidden` Balik Seketika Begitu Class Dilepas

Ada bug yang bikin kesal bukan karena susah dicari, tapi karena setengah dari fiturmu jalan sempurna. Waktu itu saya bikin drawer navigasi slide-in untuk sebuah tema toko online. Animasi bukanya cantik: panel meluncur masuk, clip-path membuka bertahap, isinya menyusul dengan skew dan fade yang berlapis. Saya sempat puas.

Lalu saya klik tombol tutup, dan drawer-nya hilang begitu saja. Bukan menutup. Bukan mundur pelan. Lenyap dalam satu frame, seperti ada yang mencabut steker.

Gejalanya: setengah koreografi hilang

Yang bikin bingung, semua transisi yang saya tulis itu simetris. Tidak ada satu pun aturan CSS khusus untuk arah tutup. Saya cuma menempelkan .is-open di elemen drawer, dan semua anaknya bereaksi ke state itu. Secara logika, melepas class yang sama harusnya membalik semuanya dengan durasi yang sama persis.

Tapi tidak. Buka: mulus. Tutup: kedip, hilang.

Ini bukan tipe bug yang muncul di console. Tidak ada error, tidak ada warning, tidak ada properti yang gagal diparsing. CSS-nya valid, JS-nya jalan, hasilnya salah.

Tersangka pertama saya keliru semua

Karena tidak ada jejak error, saya menebak ke arah JavaScript. Beberapa dugaan yang saya buang satu per satu:

Poin terakhir itu yang menutup perkara. Kalau menoggle class secara manual saja sudah salah, masalahnya murni di CSS.

Akar masalahnya: visibility itu saklar, bukan slider

Base state drawer saya seperti ini, dan tampak sangat wajar:

.drawer {
  visibility: hidden;
  pointer-events: none;
}
.drawer.is-open {
  visibility: visible;
  pointer-events: auto;
}

visibility: hidden dipakai supaya drawer yang sedang tertutup benar-benar keluar dari jangkauan: tidak bisa di-tab, tidak dibaca screen reader, tidak jadi jebakan fokus. Itu keputusan yang benar. Yang salah adalah kapan dia berlaku lagi.

Waktu .is-open dicopot, visibility: hidden dari base state langsung berlaku di frame itu juga. Tidak ada masa transisi, karena tanpa aturan transition yang eksplisit, visibility berpindah nilai secara diskret. Tidak ada posisi tengah antara terlihat dan tidak terlihat yang bisa dianimasikan.

Dan begitu elemen induknya tidak terlihat, semua anaknya ikut tidak terlihat. Transisi transform, clip-path, dan opacity di dalam drawer tetap berjalan, cuma jalannya di ruang yang tidak dirender. Selama 700 milidetik lebih, browser dengan rajin menganimasikan sesuatu yang sudah tidak ada di layar.

Jadi animasi tutupnya bukan tidak ada. Animasi tutupnya kepotong di frame pertama.

Perbaikannya: tunda flip visibility-nya

visibility memang tidak bisa dianimasikan bertahap, tapi dia bisa ditunda. Ini yang bikin triknya jalan: kasih transition-delay selama durasi tutup terpanjang, dengan durasi transisi nol.

.drawer {
  visibility: hidden;
  pointer-events: none;
  transition: visibility 0s linear 760ms; /* samakan dengan transisi balik terpanjang */
}
.drawer.is-open {
  visibility: visible;
  pointer-events: auto;
  transition: visibility 0s linear 0s; /* buka harus instan */
}
.drawer__panel { transform: translateX(100%); transition: transform 720ms ease; }
.drawer.is-open .drawer__panel { transform: translateX(0); }

Baca dua aturan itu sebagai arah yang berbeda, bukan sebagai satu nilai:

Perhatikan asimetrinya. Delay hanya diperlukan di satu arah, dan itu kenapa dua aturan ini tidak boleh disederhanakan jadi satu.

Kenapa 760ms, bukan 720ms

Transisi terpanjang di dalam drawer saya adalah transform panel, 720ms. Angka delay-nya saya set sedikit lebih besar, 760ms, supaya ada sisa ruang. Kalau nilainya dipasang persis sama, kamu bermain di batas: sedikit saja pembulatan frame atau easing yang belum benar-benar mendarat, dan ujung animasinya masih bisa terpotong.

Yang lebih penting: angka ini harus diikat ke transisi terpanjang di dalam drawer, bukan ke perasaan. Kalau nanti kamu menambah elemen dengan transisi 900ms di dalam sana, angka 760 ini langsung jadi salah lagi dan bug-nya balik dalam bentuk yang lebih halus, cuma ekor animasinya yang hilang. Simpan angkanya di CSS variable dan pakai variabel yang sama untuk keduanya kalau drawer-mu bakal berkembang.

Jangan juga kelewat royal. Delay yang jauh lebih panjang dari animasinya berarti drawer tetap visible padahal secara visual sudah selesai menutup, dan itu mengembalikan sebagian masalah aksesibilitas yang tadi mau kamu hindari.

pointer-events sengaja tidak ikut ditunda

Di kode di atas, pointer-events berpindah tanpa delay sama sekali. Itu disengaja. Begitu pengguna menekan tombol tutup, drawer harusnya langsung berhenti menerima klik, meski secara visual dia masih meluncur keluar selama tiga per empat detik. Kalau delay-nya diikutkan juga, ada jendela di mana panel yang sedang pergi masih bisa diklik, dan itu terasa seperti bug yang lain lagi.

Jadi dua properti ini kelihatan mirip, tapi perlakuannya berbeda: visibility ditahan sampai animasi selesai, pointer-events dilepas sekarang juga.

Catatan soal alternatif yang lebih baru

Kalau kamu langsung kepikiran transition-behavior: allow-discrete, hati-hati dulu. Properti itu dibuat untuk properti yang perilaku animasinya benar-benar diskret seperti display, content-visibility, dan overlay. Menempelkannya ke visibility tidak mengubah apa pun, karena visibility sendiri sudah bisa ditransisikan tanpa bantuan itu.

Rute modern untuk kasus semacam ini sebenarnya pindah dari visibility: hidden ke display: none, lalu mentransisikan display dengan allow-discrete dan menambah blok @starting-style supaya animasi masuknya ikut jalan. Buat saya waktu itu tidak perlu sejauh itu. Trik delay cuma satu baris, tidak menuntut dukungan browser tertentu, dan gampang dibaca orang lain yang buka file itu enam bulan kemudian.

Yang saya bawa pulang