Scroll-driven Animation — Web Animation

Scroll-driven Animation Scroll-driven animation menghubungkan progress animasi dengan posisi scroll. Alih-alih berjalan berdasarkan waktu, animasi bergerak maju

Scroll-driven Animation

Scroll-driven animation menghubungkan progress animasi dengan posisi scroll. Alih-alih berjalan berdasarkan waktu, animasi bergerak maju-mundur sesuai scroll pengguna.

Scroll Progress Indicator

Contoh paling klasik — progress bar yang menunjukkan berapa jauh user sudah scroll:

// JavaScript approach
window.addEventListener('scroll', () => {
  const scrollTop = document.documentElement.scrollTop;
  const scrollHeight = document.documentElement.scrollHeight;
  const clientHeight = document.documentElement.clientHeight;

  const progress = scrollTop / (scrollHeight - clientHeight);

  document.querySelector('.progress-bar').style.transform =
    \`scaleX(${progress})\`;
});
.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: #3b82f6;
  transform-origin: left;
  transform: scaleX(0);
  z-index: 50;
}

CSS scroll-timeline (Modern API)

Browser modern mendukung scroll-driven animation murni CSS — tanpa JavaScript!

/* Progress bar dengan pure CSS */
@keyframes grow-progress {
  from { transform: scaleX(0); }
  to { transform: scaleX(1); }
}

.progress-bar {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 3px;
  background: #3b82f6;
  transform-origin: left;

  animation: grow-progress auto linear;
  animation-timeline: scroll();
}

⚠️ Dukungan browser terbatas: animation-timeline: scroll() baru tersedia di Chrome/Edge 115+ (Juli 2023). Safari dan Firefox belum mendukung per April 2026. Kalau kamu targetkan pengguna umum, WAJIB sediakan fallback JavaScript. Cek support real-time di caniuse.com/css-scroll-timeline.

Fallback JavaScript (wajib untuk cross-browser)

Pola progressive enhancement: pakai CSS jika didukung, fallback ke JS kalau tidak.

// Deteksi dukungan
const supportsScrollTimeline = CSS.supports('animation-timeline: scroll()');

if (!supportsScrollTimeline) {
  // Fallback pakai scroll event + rAF throttling
  const progressBar = document.querySelector('.progress-bar');
  let ticking = false;

  window.addEventListener('scroll', () => {
    if (!ticking) {
      requestAnimationFrame(() => {
        const scrolled = window.scrollY;
        const max = document.documentElement.scrollHeight - window.innerHeight;
        const progress = scrolled / max;
        progressBar.style.transform = \`scaleX(${progress})\`;
        ticking = false;
      });
      ticking = true;
    }
  }, { passive: true });
}

Kenapa requestAnimationFrame + flag ticking? Scroll event bisa fire ratusan kali per detik — tanpa throttling browser lag. rAF memastikan update max 60×/detik sinkron dengan refresh rate. Opsi { passive: true } memberi tahu browser kita tidak akan preventDefault(), jadi scroll tetap smooth.

Parallax Effect

Elemen bergerak dengan kecepatan berbeda saat scroll, menciptakan kedalaman:

window.addEventListener('scroll', () => {
  const scrollY = window.scrollY;

  // Layer belakang bergerak lambat
  document.querySelector('.bg-layer').style.transform =
    \`translateY(${scrollY * 0.3}px)\`;

  // Layer depan bergerak normal (atau lebih cepat)
  document.querySelector('.fg-layer').style.transform =
    \`translateY(${scrollY * -0.1}px)\`;
});

Untuk performa lebih baik, gunakan rAF:

let ticking = false;

window.addEventListener('scroll', () => {
  if (!ticking) {
    requestAnimationFrame(() => {
      updateParallax(window.scrollY);
      ticking = false;
    });
    ticking = true;
  }
});

function updateParallax(scrollY) {
  document.querySelector('.hero-bg').style.transform =
    \`translateY(${scrollY * 0.5}px)\`;
}

CSS Scroll-linked Animations

/* Fade in elemen saat di-scroll ke view */
@keyframes fade-in {
  from {
    opacity: 0;
    transform: translateY(50px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.reveal-on-scroll {
  animation: fade-in auto ease-out both;
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

animation-range

Mengontrol kapan animasi mulai dan selesai relatif terhadap viewport:

.element {
  animation: fade-in auto ease both;
  animation-timeline: view();

  /* Mulai saat elemen masuk viewport,
     selesai saat 50% elemen terlihat */
  animation-range: entry 0% entry 100%;
}

/* Atau: animasi sepanjang elemen terlihat */
.element-full {
  animation: scale-up auto ease both;
  animation-timeline: view();
  animation-range: contain 0% contain 100%;
}

Contoh: Scroll-linked Header

let lastScroll = 0;

window.addEventListener('scroll', () => {
  const currentScroll = window.scrollY;
  const header = document.querySelector('.header');

  // Scroll ke bawah — sembunyikan header
  if (currentScroll > lastScroll && currentScroll > 100) {
    header.style.transform = 'translateY(-100%)';
  }
  // Scroll ke atas — tampilkan header
  else {
    header.style.transform = 'translateY(0)';
  }

  // Tambah shadow saat sudah scroll
  if (currentScroll > 10) {
    header.classList.add('shadow');
  } else {
    header.classList.remove('shadow');
  }

  lastScroll = currentScroll;
});
.header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.header.shadow {
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

Performance Tips

  1. Throttle scroll events — gunakan rAF atau passive: true
  2. Hindari read-write cycle — baca semua dulu, baru tulis semua
  3. Gunakan transform — jangan animasi top/left
  4. CSS scroll-timeline — lebih performant karena berjalan di compositor thread
// Passive listener — browser tahu kamu tidak akan preventDefault
window.addEventListener('scroll', handler, { passive: true });

Scroll-driven animation adalah salah satu teknik paling impactful untuk membuat website terasa interaktif dan engaging.