Membuat Efek Paralaks pada Scroll

Efek paralaks saat men-scroll adalah teknik visual yang memungkinkan berbagai lapisan konten bergerak dengan kecepatan berbeda saat pengguna men-scroll halaman ke bawah. Hal ini menciptakan ilusi kedalaman, dengan elemen latar depan yang tampak bergerak lebih cepat daripada elemen latar belakang, sehingga menghasilkan pengalaman penjelajahan yang menarik dan imersif. Efek ini paling baik dicapai menggunakan Animasi Berbasis Scroll CSS, yang memungkinkan Anda menautkan animasi ke posisi scroll container.

Cara menerapkan

Berikut cara membuat efek paralaks dasar:

  1. Buat elemen wrapper: Elemen ini hanya mengelompokkan semua lapisan efek paralaks. Elemen ini bukan elemen yang dapat di-scroll, sehingga overflow-nya harus dipotong. Berikan juga height yang sesuai dengan tinggi salah satu lapisan efek paralaks.

    <div class="wrapper">
      …
    </div>
    
    .wrapper {
      overflow: clip;
      height: 100vh; /* Height of one of the layers of the parallax */
    }
    
  2. Deklarasikan lapisan: Di dalam wrapper, tambahkan setiap lapisan yang akan bergerak dengan kecepatan berbeda.

    <div class="wrapper">
      <div class="layer">LAYER 0</div>
      <div class="layer">LAYER 1</div>
      <div class="layer">LAYER 2</div>
      …
    </div>
    
  3. Tambahkan animasi terjemahan: Tentukan animasi CSS yang mengubah properti transform lapisan. Untuk efek paralaks, Anda biasanya akan menggunakan translateY untuk memindahkan lapisan secara vertikal.

    @keyframes parallax {
      from {
        transform: translateY(700px);
      }
    }
    
  4. Siapkan view-timeline: Untuk menautkan animasi ke posisi scroll, buat view-timeline di elemen wrapper, lalu terapkan ke lapisan.

    .wrapper {
      view-timeline: --wrapper;
    }
    
    .layer {
      animation: parallax linear both;
      animation-timeline: --wrapper;
    }
    
  5. Atur animasi secara bertahap: Untuk membuat lapisan bergerak dengan kecepatan yang berbeda, Anda dapat menggunakan salah satu dari dua pendekatan utama: mengatur keyframe secara bertahap, atau mengatur animation-range secara bertahap.

    Kedua pendekatan ini dapat menggunakan nilai hardcode, atau dapat menggunakan implementasi sibling-index()/sibling-count(). Nilai hardcode adalah yang paling mudah dan juga berguna jika hanya memiliki jumlah lapisan yang terbatas. Implementasi sibling-index()/sibling-count() berguna saat Anda memiliki banyak lapisan.

    • Menyusun keyframe:

      Dengan menggunakan nilai hardcode, Anda dapat menentukan properti kustom untuk setiap lapisan guna mengontrol offset paralaksnya secara manual.

      .layer:nth-child(1) { --offset: 100px; }
      .layer:nth-child(2) { --offset: 200px; }
      .layer:nth-child(3) { --offset: 300px; }
      
      @keyframes parallax {
        from {
          transform: translateY(var(--offset));
        }
      }
      

      Dengan menggunakan sibling-index(), biarkan fungsi sibling-index() menampilkan indeks elemen turunan di antara elemen seinduknya untuk otomatis menghitung efek bertingkat.

      @keyframes parallax {
        from {
          transform: translateY(calc(100px * sibling-index()));
        }
      }
      
    • Menyebarkan animation-range:

      Dengan menggunakan nilai hardcode, Anda dapat menentukan batas animation-range pada setiap lapisan secara eksplisit.

      .layer:nth-child(1) { animation-range: entry 25% exit 50%; }
      .layer:nth-child(2) { animation-range: entry 25% exit 75%; }
      .layer:nth-child(3) { animation-range: entry 25% exit 100%; }
      

      Dengan menggunakan sibling-index() dan sibling-count(), Anda dapat menghitung rentang secara matematis berdasarkan jumlah total lapisan (sibling-count()).

      .layer {
        animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
      }
      

Kode contoh

@keyframes parallax {
  from {
    transform: translateY(calc(100px * sibling-index()));
  }
}

.wrapper {
  view-timeline: --wrapper;
}

.layer {
  animation: parallax linear both;
  animation-timeline: --wrapper;
}

@media (prefers-reduced-motion: reduce) {
  .layer {
    animation: none;
  }
}

Atau, Anda dapat menggunakan properti animation-range untuk mendapatkan efek yang serupa:

@keyframes parallax {
  from {
    transform: translateY(700px);
  }
}

.wrapper {
  view-timeline: --wrapper;
}

.layer {
  animation: parallax linear both;
  animation-timeline: --wrapper;
  animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
}

@media (prefers-reduced-motion: reduce) {
  .layer {
    animation: none;
  }
}

Praktik Terbaik

Saat menggunakan animasi berbasis scroll, penting untuk mengikuti beberapa praktik terbaik guna memastikan pengalaman yang lancar dan mudah diakses:

  • LAKUKAN penyertaan deteksi fitur: Tidak semua browser mendukung animasi berbasis scroll. Gunakan @supports ((animation-timeline: view()) and (animation-range: entry)) untuk memeriksa dukungan dan menyediakan penggantian untuk browser yang tidak mendukungnya.
    • Pemeriksaan (animation-range: entry) HARUS disertakan di sini, untuk memfilter browser yang hanya memiliki dukungan parsial.
    • JANGAN gunakan paket scroll-timeline-polyfill untuk strategi penggantian karena fiturnya tidak lengkap dan memiliki banyak masalah umum.
    • Jika animasi hanya dianggap sebagai dekoratif, pilih Progressive Enhancement dan JANGAN berikan penggantian.
  • LAKUKAN penghormatan terhadap preferensi pengguna: Beberapa pengguna lebih memilih untuk memiliki lebih sedikit gerakan di web. Gunakan kueri media prefers-reduced-motion untuk menonaktifkan atau mengurangi animasi bagi pengguna ini.
  • LAKUKAN upaya untuk menganimasikan hanya properti CSS yang berperforma tinggi: Untuk animasi yang paling lancar, tetap gunakan properti animasi yang dapat ditangani oleh thread compositor browser, seperti transform dan opacity. Menganimasikan properti lain seperti width atau height dapat menyebabkan masalah performa.
  • LAKUKAN penggunaan urutan deklarasi yang benar: Saat menggunakan properti singkat animation, deklarasikan animation-timeline dan animation-range setelahnya untuk mencegah properti singkat mereset linimasa.

Adapun untuk menetapkan animation-range:

  • LAKUKAN: berikan offset awal yang sama untuk semua lapisan, misalnya entry 25%
  • LAKUKAN: Berikan offset akhir yang berbeda untuk semua lapisan yang menggunakan sibling-count() dan sibling-index() untuk mendistribusikan offset, misalnya exit calc(100% / sibling-count() * sibling-index()).

Dukungan browser dan strategi penggantian

Oleh karena itu, strategi penggantian biasanya diperlukan.

Untuk browser yang tidak mendukung animasi berbasis scroll, Anda dapat menggunakan penggantian untuk membuat ulang efek visual. Penggantian biasanya dibuat dengan pemroses scroll (untuk efek ScrollTimeline) atau IntersectionObserver API (untuk efek ViewTimeline).

Di browser dengan dukungan bawaan untuk animasi berbasis scroll, SELALU gunakan implementasi CSS native karena lebih berperforma.

Perhatikan bahwa tidak semua efek dapat dibuat ulang menggunakan pendekatan penggantian.

Khusus untuk kasus penggunaan ini, skrip berikut menerapkan penggantian untuk browser yang tidak mendukung animasi berbasis scroll. Kode ini menggunakan IntersectionObserver untuk melacak visibilitas elemen .wrapper dan memperbarui properti transform lapisan berdasarkan posisi scroll.

// Fallback for browsers that don't support scroll-driven animations
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
  const wrapper = document.querySelector('.wrapper');
  const layers = document.querySelectorAll('.layer');

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        window.addEventListener('scroll', onScroll);
      } else {
        window.removeEventListener('scroll', onScroll);
      }
    });
  }, { threshold: 0 });

  observer.observe(wrapper);

  function onScroll() {
    const scrollY = window.scrollY;
    const wrapperRect = wrapper.getBoundingClientRect();
    const wrapperTop = wrapperRect.top + scrollY;
    const wrapperHeight = wrapperRect.height;
    const windowHeight = window.innerHeight;

    if (scrollY >= wrapperTop - windowHeight && scrollY <= wrapperTop + wrapperHeight) {
      const scrollPercent = (scrollY - (wrapperTop - windowHeight)) / (wrapperHeight + windowHeight);
      
      layers.forEach((layer, index) => {
        // This matches the effect as defined in the CSS example above.
        // Customize this further if needed.
        const initialTranslateY = 100 * index;
        const translateY = initialTranslateY * (1 - scrollPercent);
        layer.style.transform = `translateY(${translateY}px)`;
      });
    }
  }

  // Trigger onScroll once to set initial positions
  onScroll();
}