D
P
0

Web Animation

ScrollTrigger di Bawah Hero Rusak Setelah Aset Loading? GSAP pin vs CSS sticky

13 Juni 2026·2 menit baca
ScrollTrigger di Bawah Hero Rusak Setelah Aset Loading? GSAP pin vs CSS sticky

Ini salah satu bug animasi paling menyebalkan yang pernah saya kejar, karena ia muncul belakangan — bukan saat load, tapi setelah aset berat selesai diunduh. Di sebuah situs cinematic dengan hero frame-sequence (ratusan frame), semua reveal dan clip-animation di section bawah hero tiba-tiba meleset: trigger-nya nyala di posisi yang salah, sebagian tidak nyala sama sekali. Di awal semuanya normal.

Setelah berjam-jam, akar masalahnya: GSAP pin: true menyuntikkan pin-spacer secara runtime, dan itu mengubah tinggi dokumen setelah halaman sudah ter-layout.

Kenapa pin: true bikin semuanya meleset

Saat Anda mem-pin hero dengan ScrollTrigger (pin: true), GSAP membungkus elemen itu dengan sebuah pin-spacer — div tambahan yang menahan ruang selama hero "menempel". Spacer ini ditambahkan saat runtime, dan tingginya bisa berubah.

Masalahnya muncul dengan aset yang loading lama (frame sequence, gambar besar): begitu aset selesai dan hero mendapat dimensi final, pin-spacer tumbuh — menambah ratusan piksel ke tinggi dokumen. Tapi semua ScrollTrigger di bawahnya sudah terlanjur menghitung posisi start/end mereka terhadap layout sebelum pin. Hasilnya: semuanya bergeser.

Dan kalau Anda pakai Lenis (smooth scroll), makin parah: Lenis menyimpan batas scroll secara independen dan tidak otomatis menyesuaikan saat pin-spacer tumbuh — jadi smooth scroll dan ScrollTrigger jadi tidak sinkron.

Yang bikin frustrasi: ScrollTrigger.refresh() pun sering tidak cukup memulihkannya, apalagi dengan Lenis di tengah. Saya sudah coba invalidateOnRefresh + setTimeout(refresh, 50) — tetap drift.

Solusi: pakai CSS position: sticky, bukan GSAP pin

Daripada membiarkan GSAP memanipulasi DOM saat runtime, buat ruang scroll-nya lewat CSS sejak awal sehingga tinggi dokumen tidak pernah berubah:

/* Container luar menyediakan tinggi scroll dari awal (t=0) */
.hero-stage {
  height: calc(100svh + 1400px); /* 1400px = jarak scroll efek hero */
}
 
/* Elemen dalam yang "menempel" pakai sticky native */
.hero-sticky {
  position: sticky;
  top: 0;
  height: 100svh;
}

Lalu ScrollTrigger hanya membaca progress, tanpa pin sama sekali:

gsap.to(heroAnim, {
  // ...properti animasi
  scrollTrigger: {
    trigger: ".hero-stage",
    start: "top top",
    end: "bottom bottom",
    scrub: true, // hanya scrub, TANPA pin: true
  },
});

Karena tinggi dokumen sudah pasti sejak awal (dari calc()), pin-spacer tidak pernah disuntikkan, tinggi tidak pernah berubah saat aset loading, dan semua ScrollTrigger di bawah hero menghitung posisi terhadap layout yang stabil. Lenis pun tidak perlu re-measure.

Pola ini sudah saya pakai berulang di proyek React maupun vanilla, dan hasilnya konsisten: scroll effect hero yang panjang lebih baik pakai native sticky daripada GSAP pin.

Catatan tambahan

  • Pakai svh (small viewport height), bukan vh, supaya tidak melonjak saat address bar mobile muncul/hilang.
  • Kalau butuh frame-0 langsung tergambar sebelum scroll, paksa satu requestAnimationFrame(draw) saat load — jangan tunggu event scroll pertama.
  • Kalau tetap mau pakai GSAP pin (misal untuk horizontal scroll), pastikan semua aset yang memengaruhi tinggi sudah dimuat sebelum membuat ScrollTrigger, lalu panggil ScrollTrigger.refresh() sekali setelahnya.

Intinya: begitu sebuah library mengubah tinggi dokumen saat runtime, semua perhitungan scroll yang bergantung padanya jadi rapuh. CSS sticky memindahkan "ruang scroll" itu ke layout yang deklaratif dan stabil — dan bug "animasi bawah hero meleset setelah loading" pun hilang.