Ini bug yang bikin saya panik sebentar, karena gejalanya bukan "animasinya patah" — tapi kontennya hilang sama sekali. Di sebuah situs cinematic yang saya bangun, hero image dan beberapa kartu produk kadang-kadang tidak muncul. Bukan pudar, bukan setengah jalan: benar-benar kosong, seperti elemennya tidak ada. Refresh kadang memperbaikinya, kadang tidak. Itulah yang paling menyebalkan: tidak konsisten.
Setelah ditelusuri, akar masalahnya ada di pola scroll-reveal yang saya pakai — dan satu keputusan kecil yang fatal: elemen reveal mulai dalam keadaan tersembunyi total.
Initial state yang menyembunyikan total adalah jebakan
Pola saya begini. Setiap elemen .reveal di-clip penuh lewat CSS, lalu sebuah IntersectionObserver menambahkan kelas .is-visible saat elemen masuk viewport.
.reveal {
clip-path: inset(0 0 100% 0); /* terklip penuh — tak terlihat */
}
.reveal.is-visible {
clip-path: inset(0 0 0 0);
}const io = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) entry.target.classList.add("is-visible");
});
},
{ threshold: 0.15, rootMargin: "-10%" }
);
document.querySelectorAll(".reveal").forEach((el) => io.observe(el));Masalahnya: dengan threshold: 0.15 + rootMargin: "-10%", ada banyak edge case di mana observer tidak pernah memicu. Scroll yang sangat cepat bisa melewati elemen tanpa frame intersection yang tertangkap. Elemen yang posisinya di viewport tapi interseksinya di bawah ambang 0.15 tidak akan dihitung masuk. Quirk browser dan timing layout menambah ketidakpastian.
Dan inilah bagian fatalnya: ketika observer gagal, kelas .is-visible tidak pernah ditambahkan, sehingga clip-path: inset(0 0 100% 0) bertahan selamanya. Konten lenyap permanen — tanpa error di console, tanpa petunjuk apa pun.
Solusi: jangan pernah sembunyikan total — degradasi ke visible
Prinsip yang saya pegang sekarang: initial state sebuah reveal tidak boleh fully-hidden. Kalau trigger-nya gagal, konten harus tetap terlihat. Saya menyusun tiga lapis pertahanan.
Lapis 1 — initial state yang bisa pulih sendiri
Alih-alih klip penuh, saya pakai kombinasi properti yang masing-masing punya efek, plus klip yang hanya sesilir (8%, bukan 100%). Kalau salah satu properti gagal di-reset, properti lain tetap menampilkan konten.
.reveal {
opacity: 0;
transform: translateY(32px);
clip-path: inset(0 0 8% 0); /* sesilir, BUKAN 100% */
transition: opacity 0.8s ease, transform 0.8s ease, clip-path 0.8s ease;
}
.reveal.is-visible {
opacity: 1;
transform: translateY(0);
clip-path: inset(0 0 0 0);
}Lapis 2 — observer yang lebih toleran
Saya turunkan threshold dari 0.15 ke 0.05 dan reset rootMargin ke "0px", supaya sentuhan kecil pun cukup untuk memicu.
const io = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) entry.target.classList.add("is-visible");
});
},
{ threshold: 0.05, rootMargin: "0px" }
);Lapis 3 — safety net JavaScript
Sekitar 4 detik setelah load, saya paksa tambahkan .is-visible ke elemen .reveal mana pun yang sudah ada di viewport tapi belum punya kelas itu.
window.addEventListener("load", () => {
setTimeout(() => {
document.querySelectorAll(".reveal:not(.is-visible)").forEach((el) => {
const rect = el.getBoundingClientRect();
const inView = rect.top < window.innerHeight && rect.bottom > 0;
if (inView) el.classList.add("is-visible");
});
}, 4000);
});Dan tentu saja, hormati prefers-reduced-motion: untuk pengguna yang memintanya, langsung tampilkan tanpa animasi.
@media (prefers-reduced-motion: reduce) {
.reveal {
opacity: 1;
transform: none;
clip-path: none;
transition: none;
}
}Catatan penutup
- Initial state reveal tidak boleh fully-hidden. Kalau trigger gagal, konten yang full-hidden hilang selamanya. Selalu degradasi ke visible.
- Pakai beberapa properti, bukan satu.
opacity+transform+ klip sesilir saling menutupi kegagalan satu sama lain. - Klip pakai sliver (8%), jangan 100%. Klip 100% sama dengan menyembunyikan; sliver tetap menyisakan konten yang terlihat.
- Selalu sediakan safety net. Observer bukan jaminan; timer fallback yang mengecek viewport menutup celah edge case.
- Hormati
prefers-reduced-motion. Aksesibilitas dan ketahanan bug sering datang dari keputusan yang sama.
Pelajarannya satu kalimat: animasi reveal harus dirancang supaya kegagalan trigger berarti konten tetap muncul, bukan lenyap.
