D
P
0

CSS & Web Animation

Section Hilang Permanen Setelah `gsap.from()` + ScrollTrigger? Inline `opacity:0` yang Tak Pernah Dihapus

27 Juli 2026·4 menit baca
Section Hilang Permanen Setelah `gsap.from()` + ScrollTrigger? Inline `opacity:0` yang Tak Pernah Dihapus

Ada satu bug yang bikin saya menatap layar cukup lama sambil mikir "ini section-nya ke mana." Saya lagi rebuild theme WordPress custom untuk situs klien dari versi statis, dan homepage-nya punya satu pinned section di tengah. Begitu saya scroll melewati pinned section itu, semua section di bawahnya lenyap. Bukan blank putih, tapi benar-benar tidak kelihatan: teks, gambar, tombol, semua invisible. Anehnya, link-nya masih bisa di-hover. Kursor berubah jadi pointer di tempat yang seharusnya ada tombol, tapi visualnya nol.

Gejalanya

Petunjuk pertama datang dari hover itu. Kalau elemen benar-benar display:none atau ke-unmount, dia tidak akan bisa di-hover. Tapi ini bisa. Artinya elemennya ada di DOM, menempati ruang, cuma tak terlihat. Saya buka DevTools, inspect satu heading yang harusnya muncul, dan di situ jawabannya: style="opacity:0" inline, ditempel langsung di elemen. Bukan dari CSS saya, bukan dari class. Inline style, dan yang bisa nulis inline style di elemen itu cuma satu: GSAP.

Semua reveal di section-section itu saya bikin pakai gsap.from(). Polanya kira-kira begini:

gsap.from(".reveal", {
  opacity: 0,
  y: 40,
  scrollTrigger: {
    trigger: ".reveal",
    start: "top 80%",
  },
});

Idenya sederhana: elemen mulai dari opacity 0 dan y 40, lalu animasi ke posisi natural saat trigger masuk viewport. Di lokal, dengan koneksi cepat, ini jalan mulus. Di produksi, section di bawah pin stuck hidden permanen.

Menelusuri akar masalah

Kunci gsap.from() yang jarang orang sadari: dia menaruh nilai "from" sebagai inline style DULUAN, lalu mengandalkan tween untuk menganimasikannya kembali ke nilai natural. Jadi opacity:0 itu bukan efek samping, itu memang state awal yang sengaja dipasang. Selama trigger-nya fire dan tween-nya jalan, opacity naik balik ke 1 dan inline style dibersihkan. Masalahnya: kalau trigger-nya TIDAK pernah fire, inline opacity:0 itu tidak pernah dihapus. Elemen terkunci invisible selamanya.

Kenapa trigger-nya tidak fire? Ini gabungan dua hal. Pertama, ada pinned section di atasnya. Kedua, di dalamnya ada marquee logo partner yang di-hotlink dari domain lain, jadi loading-nya telat. Saat GSAP menghitung posisi start dan end semua ScrollTrigger, gambar-gambar itu belum punya dimensi. Begitu mereka selesai load, tinggi dokumen berubah, tapi posisi trigger yang sudah dihitung tadi jadi stale. Section di bawah pin sekarang punya start yang meleset jauh, sampai ke titik yang tak akan pernah dilewati scroll. onEnter tidak pernah kepanggil, opacity:0 tidak pernah dilepas.

Perlu saya tekankan, ini bukan soal pin-spacer yang mengubah tinggi lalu saya harus buang pin-nya. Pin-nya saya pertahankan. Bug-nya murni ada di semantik gsap.from(): dia menempel state hidden sebagai inline style, dan hidup-matinya elemen tergantung apakah trigger sempat menghapusnya.

Ada varian kedua yang lebih menyebalkan lagi. Judul-judul section saya reveal pakai SplitText, word-rise per kata, juga lewat gsap.from(). Elemen yang sempat muncul normal tiba-tiba jadi pudar setiap kali ada ScrollTrigger.refresh() jalan, misalnya saat resize atau saat saya panggil refresh manual buat benerin posisi. Penyebabnya sama persis dengan sifat gsap.from(): waktu refresh, GSAP me-recapture nilai SEKARANG sebagai from-value baru. Karena elemennya sudah di posisi akhir, yang ke-capture jadi from-value-nya malah state akhir, dan hasilnya elemen dikunci di tengah, pudar.

Perbaikannya

Perbaikan intinya: berhenti pakai gsap.from() untuk reveal yang dipicu scroll. Saya ganti dengan gsap.set() untuk state awal, plus gsap.to() dengan scrollTrigger.once:

gsap.set(".reveal", { opacity: 0, y: 40 });
 
gsap.to(".reveal", {
  opacity: 1,
  y: 0,
  duration: 0.8,
  scrollTrigger: {
    trigger: ".reveal",
    start: "top 80%",
    once: true,
  },
});

once: true itu kuncinya. Dia fire sekali di first cross, lalu ScrollTrigger-nya berhenti. Tidak ada lagi re-apply state hidden, tidak ada lagi re-capture saat refresh. Kalaupun trigger meleset karena posisi stale, saya masih punya jaring pengaman kedua.

Jaring pengaman itu: jangan opacity-hide marquee partner sama sekali. Elemen yang paling sering bikin tinggi berubah justru gambar hotlink yang telat. Saya biarkan dia visible dari awal lewat CSS, pakai rule yang menetralisir state motion, kira-kira .has-motion .reveal-partners { opacity: 1 }. Dengan begitu, walau JS-nya gagal fire, elemen tidak pernah invisible.

Ketiga, buat gambar yang telat, saya panggil ScrollTrigger.refresh() lagi sekitar 900ms setelah load, biar posisi trigger dihitung ulang setelah dimensi final masuk:

window.addEventListener("load", () => {
  setTimeout(() => ScrollTrigger.refresh(), 900);
});

Terakhir, untuk reveal SplitText yang tadi jadi pudar, saya ganti gsap.from() jadi gsap.fromTo(). Dengan fromTo, nilai awal dan akhir saya tulis eksplisit, jadi ScrollTrigger.refresh() tidak punya ruang untuk salah menebak dan me-recapture state akhir sebagai from-value:

gsap.fromTo(
  words,
  { yPercent: 100, opacity: 0 },
  { yPercent: 0, opacity: 1, stagger: 0.05, scrollTrigger: { trigger: heading, once: true } }
);

Stack-nya GSAP 3.13.0 (di versi ini SplitText dan DrawSVGPlugin sudah gratis), di-enqueue dari jsdelivr.

Catatan

  • gsap.from() menaruh state awal sebagai inline style dan mengandalkan trigger untuk menghapusnya. Kalau trigger tidak pernah fire, elemen terkunci invisible.
  • Elemen invisible tapi masih bisa di-hover berarti inline opacity:0, bukan display:none. Inspect elemennya, cari inline style.
  • Untuk reveal scroll, pakai gsap.set() plus gsap.to({ scrollTrigger: { once: true } }), bukan gsap.from(). once mencegah re-apply state hidden.
  • Jangan opacity-hide elemen yang loading-nya telat (hotlink, gambar besar). Biarkan visible via CSS sebagai jaring pengaman.
  • Panggil ScrollTrigger.refresh() sekali lagi setelah aset telat masuk, biar posisi trigger tidak stale.
  • Kalau ScrollTrigger.refresh() bikin reveal jadi pudar, ganti gsap.from() dengan gsap.fromTo() yang nilainya eksplisit.