D
P
0

CSS & Web Animation

Teks Putih Lenyap di Background Terang Saat Scroll? Warna Teks Di-hardcode Desync dari Morph Background

30 Juli 2026·4 menit baca
Teks Putih Lenyap di Background Terang Saat Scroll? Warna Teks Di-hardcode Desync dari Morph Background

Saya lagi merapikan homepage untuk situs klien, sebuah tema WordPress premium dengan konsep "scenes". Idenya sinematik: warna seluruh halaman berubah pelan mengikuti scroll. Begitu kamu scroll melewati ambang tertentu, background morph dari terang ke gelap dan sebaliknya, mulus dalam sekitar .8 detik. Efeknya bagus di demo. Masalahnya baru kelihatan pas QA: di scene-scene terang, headline dan body text-nya lenyap. Bukan blur, bukan ketutup elemen lain, benar-benar hilang seperti tinta putih yang tumpah di kertas putih.

Begitu saya scroll balik ke scene gelap, teksnya muncul lagi utuh. Jadi jelas ini ada hubungannya dengan morph warna, bukan dengan font atau layout. Teksnya putih, dan di background terang, putih di atas nyaris-putih memang tidak kebaca.

Gejalanya

Saya mulai dari yang paling gampang. Buka DevTools, inspect headline yang "hilang" itu, dan warnanya memang color: #fff mati, dihardcode langsung di CSS scene. Sementara background body-nya di scene terang ada di sekitar rgb(224, 224, 224). Putih di atas abu sangat terang, kontrasnya nyaris nol. Itu jawaban untuk kenapa scene terang berteks putih tidak kebaca.

Tapi ada gejala kedua yang lebih membingungkan. Sambil scroll pelan-pelan melintasi ambang morph, ada window singkat, sepersekian detik, di mana teks dan background sama-sama gelap. Black-on-black. Teksnya lenyap lagi, tapi kali ini justru saat pindah ke scene gelap. Dua gejala yang kelihatannya berlawanan: hilang di terang, dan berkedip hilang tepat di tengah transisi.

Jalan buntu

Tebakan pertama saya salah arah. Saya kira ini soal stacking: mungkin ada overlay gradien yang naik ke atas teks pas morph. Saya cek z-index, saya matikan overlay-nya satu per satu. Teksnya tetap hilang. Saya curiga mix-blend-mode di suatu lapisan, saya buang semua blend mode. Tetap hilang. Saya sempat mikir ini masalah opacity yang keanimasi, padahal opacity teksnya konsisten 1 dari awal.

Perbaikan "cepat" yang saya coba berikutnya malah bikin gejala kedua makin parah. Saya pikir gampang: tinggal ikutkan warna teks ke class yang sama yang men-toggle skema warna. Morph-nya dikendalikan GSAP ScrollTrigger yang menempelkan class scheme-dark ke elemen html saat scroll melewati ambang 50%. Jadi saya tambahkan aturan: kalau html.scheme-dark, teks jadi putih, kalau tidak, teks jadi gelap. Endpoint-nya sekarang benar. Di scene terang teks gelap, di scene gelap teks putih.

Tapi transisi di antaranya jadi kacau. Background body punya transition: background-color .8s ease-in-out, jadi dia melar pelan selama .8 detik. Warna teks yang baru saya tambahkan tidak punya transition sama sekali, jadi dia snap seketika begitu class-nya nyala. Hasilnya: teks langsung lompat ke putih di frame pertama, sementara background masih separuh jalan di warna terang. Selama .8 detik itu, keduanya tidak cocok. Itulah kedipan black-on-black dan white-on-light yang saya lihat. Endpoint benar, tapi frame-frame di tengahnya salah.

Akar masalahnya

Di titik ini akarnya jelas. Warna teks statis, sementara warna background dianimasi. Selama dua warna itu digerakkan oleh timing yang berbeda, mereka pasti desync di window transisi. Tidak peduli endpoint-nya sudah benar. Yang bikin bug ini kelihatan adalah morph-nya lambat dan terlihat mata: .8 detik itu cukup panjang untuk mata menangkap frame yang mismatch.

Aturannya sederhana tapi mudah kelewat: kalau background sebuah komponen dianimasi, setiap warna foreground di atasnya harus ikut bertransisi dengan trigger yang sama dan timing yang sama. Bukan cuma tujuan akhirnya yang harus benar, tapi seluruh jalur di antaranya.

Perbaikannya

Kunci perbaikannya: gerakkan warna teks dari trigger yang sama persis dengan background, dan kasih dia durasi transisi yang identik. Background sudah dipicu class html.scheme-dark dengan .8s ease-in-out, jadi teksnya harus meniru itu tepat.

body {
  background-color: rgb(224, 224, 224);
  transition: background-color 0.8s ease-in-out;
}
html.scheme-dark body {
  background-color: rgb(33, 33, 33);
}
 
.scene--ink-light {
  color: rgb(45, 42, 42);
  transition: color 0.8s ease-in-out;
}
html.scheme-dark .scene--ink-light {
  color: rgb(255, 255, 255);
}

Sekarang begitu ScrollTrigger menyalakan class-nya, background dan teks bergerak dalam satu lockstep. Keduanya berangkat di frame yang sama, dengan easing yang sama, dan mendarat di frame yang sama .8 detik kemudian. Window desync-nya hilang karena secara harfiah tidak ada lagi selisih timing untuk dilihat.

ScrollTrigger.create({
  trigger: ".scene--dark",
  start: "top 50%",
  end: "bottom 50%",
  onToggle: (self) =>
    document.documentElement.classList.toggle("scheme-dark", self.isActive),
});

Satu langkah lagi yang bikin ini rapi jangka panjang: elemen dekoratif seperti titik, garis border, link, dan em tadinya masing-masing punya warna hardcode sendiri. Tiap satu itu bug desync yang menunggu giliran. Saya ubah semuanya jadi mewarisi tinta scene lewat currentColor dan opacity, bukan menyimpan nilai warna sendiri.

.scene__dot,
.scene__border,
.scene a,
.scene em {
  color: currentColor;
  border-color: currentColor;
  opacity: 0.7;
}

Dengan begitu cuma ada satu sumber warna tinta per scene, dan semua elemen ikut satu transisi yang sama. Saya ukur hasilnya lewat DevTools: sebelum morph, background rgb(224, 224, 224) dengan tinta rgb(45, 42, 42). Sesudah morph, background rgb(33, 33, 33) dengan tinta rgb(255, 255, 255). Kontrasnya sehat di kedua ujung, dan mulus di antaranya.

Checklist

  • Kalau sebuah komponen menganimasi warna background, setiap warna foreground di atasnya harus bertransisi dengan trigger dan timing yang sama. Endpoint benar saja tidak cukup.
  • Jangan biarkan warna teks snap seketika saat background-nya melar pelan. Selisih timing sekecil apa pun akan bikin kedipan mismatch di window transisi.
  • Jaga durasi tetap identik: transisi background body dan transisi warna tinta scene keduanya harus .8s ease-in-out. Mendesync durasinya akan memunculkan bug ini lagi.
  • Pakai currentColor plus opacity untuk elemen dekoratif biar mewarisi satu tinta yang dianimasi, bukan masing-masing menyimpan warna hardcode sendiri.
  • Uji di tengah scroll, bukan cuma di atas dan bawah scene. Bug ini hidup di window transisi, bukan di endpoint.