D
P
0

CSS & Web Animation

`--site-colour` Stuck Gelap Setelah Section Pinned? Pin ScrollTrigger Menggeser Koordinat Trigger

29 Juli 2026·4 menit baca
`--site-colour` Stuck Gelap Setelah Section Pinned? Pin ScrollTrigger Menggeser Koordinat Trigger

Saya kerjakan versi baru homepage untuk sebuah situs klien: arah desain "Scenes", satu halaman panjang yang paletnya berubah pelan mengikuti scroll. Mekaniknya sederhana di permukaan. Satu CSS custom property --site-colour duduk di root dan jadi sumber warna untuk background dan teks seluruh halaman. Tiap scene punya warnanya sendiri, dan begitu kamu scroll, --site-colour di-set ulang mengikuti scene yang sedang tampil.

Di tengah halaman ada satu section "project theatre" setinggi 100svh yang saya pin pakai ScrollTrigger, biar dia diam menempel sementara isinya beranimasi. Di atas kertas mulus. Masalahnya baru muncul setelah section itu: begitu saya scroll melewati theatre, --site-colour nyangkut di warna gelap. Section About dan Why di bawahnya, yang seharusnya terang dengan teks gelap, malah ikut gelap semua. Teksnya nyaris tak terbaca.

Yang bikin lama ketemu: probe DOM numerik saya lolos. Saya punya cek kecil yang membaca nilai computed --site-colour di beberapa titik scroll lalu membandingkannya dengan yang diharapkan, dan hasilnya hijau. Bug ini akhirnya ketahuan bukan dari angka, tapi dari screenshot. Mata saya yang nangkap, bukan probe.

Jalan buntu yang saya coba

Karena ini soal pin, tebakan pertama saya klasik: ScrollTrigger belum re-measure setelah layout berubah. Saya panggil ScrollTrigger.refresh() setelah semua pin dibuat. Tidak berubah. Saya tambah invalidateOnRefresh: true, saya bungkus refresh dalam requestAnimationFrame, saya coba refresh setelah aset selesai load. Warna tetap nyangkut gelap lewat theatre.

Tebakan kedua: mungkin ada class dark yang tidak dilepas, atau ini perang specificity CSS. Saya telusuri, ternyata tidak ada. --site-colour memang di-set ke nilai gelap, dan value itu betul-betul menempel di :root. Jadi CSS-nya benar. Yang salah adalah kapan, dan di posisi scroll mana, value itu di-set.

Di titik ini saya berhenti menebak dan mulai memakai screenshot sebagai alat, bukan cuma bukti. Saya scroll pelan sambil memotret tiap beberapa ratus piksel, lalu cocokkan tiap posisi scroll dengan scene yang seharusnya aktif.

Akar masalahnya

Warna scene digerakkan oleh ScrollTrigger posisional. Tiap scene punya trigger sendiri dengan start dan end berbasis posisi, kira-kira begini:

scenes.forEach((scene) => {
  ScrollTrigger.create({
    trigger: scene,
    start: "top center",
    end: "bottom center",
    onEnter: () => setColour(scene.dataset.colour),
    onEnterBack: () => setColour(scene.dataset.colour),
  });
});

Selama tidak ada pin, ini jalan. Tapi begitu section theatre di-pin, ScrollTrigger menyisipkan pin-spacer dan menggeser koordinat scroll untuk segala sesuatu yang ada di bawah pin. Trigger warna untuk About dan Why dihitung terhadap koordinat yang kemudian digeser oleh pin. Akibatnya onEnter dan onEnterBack mereka meleset: event yang seharusnya men-switch --site-colour kembali ke terang saat kamu masuk About tidak menyala di posisi scroll yang benar. Warna terakhir yang sempat di-set adalah warna gelap dari dalam theatre, dan tidak ada trigger yang menimpanya di waktu yang tepat. Jadi dia nyangkut gelap.

Dan inilah kenapa probe numerik saya lolos: probe membaca --site-colour di titik-titik yang kebetulan benar, dan value-nya memang selalu ada, tidak pernah kosong. Kegagalannya bukan "value tidak ke-set", tapi "value yang salah ke-set di scroll yang salah". Cek angka tidak melihat itu. Hanya render yang dilihat mata yang melihatnya.

Perbaikannya

Saya berhenti melawan pin dan membuang seluruh pendekatan posisional untuk warna. Ganti dengan satu scanner garis tengah: tiap frame scroll, baca getBoundingClientRect() tiap scene, lalu cari scene mana yang sedang memotong garis tengah vertikal viewport. Scene itulah yang menentukan --site-colour. Tidak ada start atau end yang perlu dihitung ulang, dan tidak ada yang bisa digeser pin.

const scenes = gsap.utils.toArray(".scene");
 
function updateSiteColour() {
  const mid = window.innerHeight / 2;
  for (const scene of scenes) {
    const rect = scene.getBoundingClientRect();
    if (rect.top <= mid && rect.bottom >= mid) {
      document.documentElement.style.setProperty(
        "--site-colour",
        scene.dataset.colour
      );
      break;
    }
  }
}
 
ScrollTrigger.create({
  trigger: document.body,
  start: 0,
  end: "max",
  onUpdate: updateSiteColour,
});

getBoundingClientRect() mengembalikan posisi relatif terhadap viewport saat itu juga, jadi pin-spacer, perubahan tinggi dokumen, atau apa pun yang menggeser layout tidak lagi jadi soal. Yang aktif adalah scene yang benar-benar ada di depan mata di garis tengah layar. Setelah ganti ini, saya verifikasi ulang section per section secara visual, dan About serta Why kembali terang dengan teks gelap seperti seharusnya.

Satu catatan penting: saya tulis keputusan ini di catatan "jangan diutak-atik". Kode scanner garis tengah kelihatan seperti umpan refactor. Nanti ada yang lihat, merasa positional ScrollTrigger lebih "bersih", lalu mengembalikannya, dan bug gelap yang sama muncul lagi. Jadi alasannya saya dokumentasikan tepat di sebelah kodenya.

Pelajaran

  • Kalau warna atau state visual nyangkut tepat setelah section yang di-pin, curigai koordinat trigger yang digeser pin, bukan CSS-nya.
  • ScrollTrigger.refresh() tidak selalu menyelamatkan. Kalau desainnya rapuh terhadap pergeseran koordinat, ganti desainnya.
  • Untuk "scene mana yang aktif", scanner garis tengah pakai getBoundingClientRect() per frame itu pin-immune by construction, bukan tergantung start dan end posisional.
  • Probe numerik bisa lolos sementara render tetap salah. Untuk bug warna, screenshot lebih jujur daripada cek angka.
  • Kalau sebuah solusi kelihatan seperti umpan refactor, catat alasannya di sebelah kodenya biar tidak dikembalikan ke versi yang bug.