aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Frontend & UI

Scroll Animasyon: Intersection Observer ile Performanslı Efektler

İyi bir scroll animasyon kurgusu, sayfayı aşağı kaydırdıkça öğelerin tam zamanında belirmesini sağlar; ne erken patlar ne de geç kalır. Yıllarca bu işi scroll olayını dinleyip her kareyi elle hesaplayarak yaptık. Artık tarayıcı bu işi bizim için, ana iş parçacığını yormadan yapabiliyor: IntersectionObserver. Bu yazıda bir öğenin görünüme girip girmediğini verimli biçimde izlemeyi, bunu CSS ile birleştirip akıcı geçişler üretmeyi ve performans ile erişilebilirlik tuzaklarından kaçınmayı adım adım anlatıyorum.

Neden scroll olayı değil de Intersection Observer?

Klasik yöntemde window.addEventListener('scroll', ...) ile her kaydırma adımında her öğe için getBoundingClientRect() çağırırdık. İki sorun var: scroll olayı saniyede onlarca kez tetiklenir ve getBoundingClientRect() tarayıcıyı yeniden hesaplamaya (layout/reflow) zorlar. Bu kombinasyon ana iş parçacığını tıkar, kaydırma takılır.

Intersection Observer ise tersine çalışır: sen tarayıcıya "şu öğe görünüme girince bana haber ver" dersin, tarayıcı bunu kendi içinde, asenkron ve optimize edilmiş şekilde izler. Geri çağrı yalnızca kesişim durumu değiştiğinde çalışır, her kaydırma karesinde değil. Sonuç: daha az kod, daha az reflow, daha akıcı sayfa.

İlk gözlemciyi kurmak

Temel kullanım üç parçadan oluşur: gözlemlenecek hedef öğeler, kesişim olunca çalışacak geri çağrı ve davranışı ayarlayan seçenekler.

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      // bir kez göründükten sonra izlemeyi bırak
      observer.unobserve(entry.target);
    }
  });
}, {
  threshold: 0.2,        // öğenin %20'si görününce tetikle
  rootMargin: '0px 0px -10% 0px'
});

document.querySelectorAll('.reveal').forEach((el) => {
  observer.observe(el);
});

Burada işin püf noktası şu: JavaScript yalnızca bir CSS sınıfı (is-visible) ekliyor. Asıl animasyonu CSS yapıyor. Bu ayrım hem performans hem de bakım açısından kritik.

Animasyonu CSS'e bırakmak

Öğenin başlangıç ve bitiş durumunu CSS'te tanımlayıp aradaki geçişi transition ile yumuşatıyoruz. transform ve opacity kullanmak önemli: bu iki özellik GPU üzerinde işlenir ve düzen (layout) yeniden hesaplaması tetiklemez.

.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.reveal.is-visible {
  opacity: 1;
  transform: translateY(0);
}

Aynı yapıyla soldan kayma, ölçeklenme veya hafif dönme gibi varyasyonları farklı sınıflarla üretebilirsin. Animasyonu CSS'te tutmanın bir başka avantajı: JavaScript çalışmasa bile içerik opacity: 0 ile gizli kalmaz; ilerleyen "erişilebilirlik" bölümünde bunu güvene alacağız.

threshold ve rootMargin'i doğru ayarlamak

Bu iki seçenek scroll animasyon hissini tümüyle belirler:

  • threshold: 0 ile 1 arası bir oran. 0 öğenin tek pikseli görününce, 1 ise tamamı görününce tetikler. Bir dizi de verebilirsin ([0, 0.25, 0.5, 1]) ve her eşikte geri çağrıyı alırsın.
  • rootMargin: Kök çerçevesini büyütüp küçültür; CSS margin sözdizimini kullanır. -10% gibi negatif bir alt değer, öğe ekranın biraz içine girmeden tetiklemeyi geciktirir, böylece animasyon "tam zamanında" hissedilir.
  • root: Varsayılan olarak görüntü alanıdır (null). Kaydırılabilir bir kapsayıcı içinde izleme yapacaksan o öğeyi root olarak verirsin.

Kademeli (stagger) giriş efekti

Kartların art arda, hafif gecikmeyle belirmesi modern arayüzlerde çok sevilir. Bunu her öğeye CSS değişkeni ile gecikme atayarak temiz biçimde yapabilirsin:

const items = document.querySelectorAll('.reveal');
const io = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (!entry.isIntersecting) return;
    const i = Number(entry.target.dataset.index || 0);
    entry.target.style.setProperty('--delay', `${i * 80}ms`);
    entry.target.classList.add('is-visible');
    io.unobserve(entry.target);
  });
}, { threshold: 0.15 });

items.forEach((el, i) => {
  el.dataset.index = i;
  io.observe(el);
});

CSS tarafında geçişe bu gecikmeyi bağlarsın: transition-delay: var(--delay, 0ms);. Böylece dört kart sırayla, akıcı bir dalga halinde belirir.

Performans ve erişilebilirlik tuzakları

Birkaç noktaya dikkat etmek, projeyi profesyonel kılar:

  • Tekrar tetiklemeyi yönet: Öğe bir kez göründükten sonra unobserve() ile izlemeyi bırak; aksi halde kullanıcı yukarı kaydırınca animasyon sıfırlanır. Tekrar oynamasını istiyorsan is-visible sınıfını !entry.isIntersecting durumunda kaldır.
  • Hareket azaltma tercihi: Bazı kullanıcılar hareketten rahatsız olur. @media (prefers-reduced-motion: reduce) ile geçişleri kapat ve içeriği doğrudan görünür yap.
  • JavaScript kapalıysa içerik gizli kalmasın: Başlangıç gizliliğini yalnızca JS aktifken uygula. Pratik bir yol: <html> kök öğesine document.documentElement.classList.add('js') ekleyip CSS'i .js .reveal { opacity: 0; } şeklinde koşullamaktır.
@media (prefers-reduced-motion: reduce) {
  .reveal {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

Sık Sorulan Sorular

Intersection Observer tarayıcı desteği nasıl?

Tüm güncel tarayıcılarda (Chrome, Firefox, Safari, Edge) yıllardır destekleniyor; ayrı bir kütüphaneye gerek yok. Çok eski tarayıcıları hedeflemiyorsan polyfill'e de ihtiyacın olmaz.

requestAnimationFrame ile scroll dinlemek daha mı iyi?

Sürekli ilerleyen (scroll boyunca değişen) parallax gibi efektler için requestAnimationFrame uygundur. Ama bir öğenin görünüp görünmediğini bilmek için Intersection Observer her zaman daha verimli ve sade çözümdür.

Animasyon her kaydırışta tekrar oynamalı mı?

Genelde hayır. İçerik bir kez belirsin ve öyle kalsın; sürekli tekrar eden giriş animasyonları dikkat dağıtır ve performansı yorar. İstisnai durumlarda tekrar oynatabilirsin ama varsayılan olarak unobserve() önerilir.

Sitende akıcı, performanslı scroll animasyonları mı istiyorsun? Doğru eşiklerle ayarlanmış, erişilebilir ve takılmayan arayüz efektleri için benimle iletişime geç.

Bu kategorideki tüm yazılar →

Devamı için