Scroll-driven animations: JS'siz parallax
Bir sayfada aşağı kaydırdıkça dolacak bir progress bar yapmak için ne kadar JavaScript yazdığınızı düşünün. scroll event listener, requestAnimationFrame, scrollTop / scrollHeight hesabı, throttle fonksiyonu. Hepsi birkaç satır CSS ile değiştirilebilir durumda. Chrome 115'ten (Temmuz 2023) bu yana tarayıcıda olan scroll-driven animations API'si, animasyonların zaman çizelgesini scroll pozisyonuna ya da bir elementin viewport'taki görünürlüğüne bağlıyor.
Temel kavramlar
CSS animasyonları normalde zamana bağlı çalışır. animation-duration: 2s derseniz animasyon 2 saniyede tamamlanır. Scroll-driven animations'da bu zaman çizelgesi scroll container'ın kaydırma mesafesiyle ya da bir elementin görünürlük yüzdesiyle değiştirilir. Bunu yapan iki yeni CSS fonksiyonu var:
scroll() fonksiyonu animasyonu bir scroll container'ın kaydırma ilerlemesine bağlar. Sayfa en tepedeyken animasyon %0, en alttayken %100 olur.
view() fonksiyonu ise animasyonu bir elementin scroll container içinde görünür olma sürecine bağlar. Element viewport'a girmeye başladığında animasyon başlar, tamamen çıktığında biter.
Her ikisi de animation-timeline property'si üzerinden kullanılır.
Scroll progress bar
En klasik örnek: sayfanın ne kadarının okunduğunu gösteren üst çubuk. JavaScript ile yapıldığında main thread'i meşgul eder, scroll jank riski doğurur. CSS ile yapıldığında compositor thread'de çalışır, sıfır performans maliyetiyle.
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
width: 100%;
background: #3b82f6;
transform-origin: left;
animation: grow-bar linear;
animation-timeline: scroll(root block);
}
@keyframes grow-bar {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}<div class="progress-bar"></div>Burada scroll(root block) ifadesi animasyonu root scroller'ın (yani viewport'un) dikey kaydırma ilerlemesine bağlıyor. root yerine nearest yazarsanız en yakın scroll ancestor'ı kullanır. block yerine inline yazarsanız yatay scroll'a bağlarsınız.
Dikkat edilecek nokta: animation-duration belirtmeye gerek yok. Zaman çizelgesi artık scroll mesafesi olduğu için süre kavramı devreden çıkıyor. Ama animation-timing-function hâlâ geçerli; linear yerine ease-in-out koyarak farklı hız eğrileri elde edebilirsiniz.
Parallax efekti
Parallax her zaman JavaScript ağırlıklı bir iş oldu. IntersectionObserver ile tetikleme, scroll offset hesaplama, transform güncelleme. Scroll-driven animations ile bunların hiçbirine gerek kalmıyor.
<section class="parallax-section">
<img class="parallax-bg" src="/hero.jpg" alt="Hero">
<h1>Başlık</h1>
</section>.parallax-section {
position: relative;
height: 100vh;
overflow: hidden;
}
.parallax-bg {
position: absolute;
inset: -20% 0;
width: 100%;
height: 140%;
object-fit: cover;
animation: parallax-shift linear;
animation-timeline: scroll(nearest block);
}
@keyframes parallax-shift {
from {
transform: translateY(0);
}
to {
transform: translateY(-15%);
}
}Görsel, parent'ından %40 daha uzun. Sayfa kaydırıldıkça görsel yukarı kayıyor, klasik parallax illüzyonu oluşuyor. nearest parametresi animasyonu en yakın scrollable ancestor'a bağlıyor; bu örnekte bu viewport oluyor çünkü .parallax-section kendisi scroll etmiyor.
view() ile elementin viewport'a giriş animasyonu
Bir kartın viewport'a girdiğinde fade-in yapmasını istiyorsanız, eskiden IntersectionObserver kurup class toggle yapardınız. view() fonksiyonu bunu doğrudan CSS'e taşıyor.
.card {
animation: fade-slide-in linear both;
animation-timeline: view(block);
animation-range: entry 0% entry 100%;
}
@keyframes fade-slide-in {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}Burada animation-range kritik bir property. entry 0% elementin viewport'a girmeye başladığı an, entry 100% ise tamamen girdiği an. Yani animasyon, element viewport'un alt kenarından görünmeye başladığı andan tamamen göründüğü ana kadar ilerliyor.
animation-range değerleri
animation-range dört ana aralık kabul ediyor:
entry: element viewport'a giriyorexit: element viewport'tan çıkıyorcontain: element tamamen viewport içindecover: element viewport'la herhangi bir kesişimde (entry başlangıcından exit sonuna kadar)
Bunları yüzdelerle birleştirebilirsiniz. Mesela animation-range: entry 20% entry 80% derseniz animasyon, elementin %20 göründüğü anda başlar, %80 göründüğünde tamamlanır. Bu, animasyonun ne kadar agresif ya da yumuşak hissettirdiğini doğrudan kontrol ediyor.
Named scroll timeline ile iç içe scroll container'lar
Bazen animasyonu bağlamak istediğiniz scroll container, CSS'in otomatik bulacağı nearest ancestor değildir. Bu durumda scroll-timeline-name ile timeline'a isim verip referans alabilirsiniz.
.custom-scroller {
overflow-y: auto;
height: 400px;
scroll-timeline-name: --my-scroller;
scroll-timeline-axis: block;
}
.indicator {
animation: progress linear;
animation-timeline: --my-scroller;
}
@keyframes progress {
from { width: 0%; }
to { width: 100%; }
}Bu teknik, sayfa içindeki bağımsız scroll alanlarında (bir sidebar, bir modal içindeki liste) ilerleme göstergesi yapmak için işe yarıyor.
Birden fazla animasyonu birleştirme
Bir elemente hem scroll-based hem view-based animasyon bağlamak mümkün. Birden fazla animation tanımı virgülle ayrılarak yazılır, her birine farklı timeline atanır.
.hero-text {
animation:
blur-out linear both,
color-shift linear both;
animation-timeline:
scroll(root),
view();
animation-range:
0% 50%,
entry 0% entry 100%;
}
@keyframes blur-out {
to {
filter: blur(8px);
opacity: 0.3;
}
}
@keyframes color-shift {
from { color: #1e293b; }
to { color: #3b82f6; }
}İlk animasyon sayfanın ilk %50'sinde bulanıklaşma yapar, ikincisi element viewport'a girerken renk değiştirir. İki bağımsız timeline aynı element üzerinde çalışır.
Tarayıcı desteği ve fallback stratejisi
Chrome 115+, Edge 115+ ve Opera 101+ tam destek veriyor. Firefox 110'da scroll-timeline flag arkasında mevcut ama varsayılan olarak kapalı (Nisan 2025 itibarıyla). Safari'de henüz yok; WebKit'in tracker'ında "In Development" statüsünde.
Fallback stratejisi basit: @supports kullanın.
.progress-bar {
/* Fallback: statik görünüm */
transform: scaleX(0);
}
@supports (animation-timeline: scroll()) {
.progress-bar {
animation: grow-bar linear;
animation-timeline: scroll(root block);
}
}Scroll-driven animation desteklenmeyen tarayıcılarda progress bar görünmez, ama sayfa normal çalışmaya devam eder. Bu progressive enhancement mantığı, view transitions API'sinin MPA'lardaki kullanımıyla benzer bir yaklaşım.
Performans neden iyi?
JavaScript ile scroll event dinlediğinizde her frame'de main thread çalışır. requestAnimationFrame kullansanız bile layout thrashing riski vardır (scrollTop okuyup DOM yazarsanız). Scroll-driven animations ise transform ve opacity gibi compositor-friendly property'leri kullandığında tamamen compositor thread'de çalışır. Main thread'e dokunmaz. Bu yüzden 60fps'in altına düşme riski pratikte sıfıra yakındır.
Ama bir uyarı: animasyonda width, height, top, left gibi layout tetikleyen property'ler kullanırsanız bu avantajı kaybedersiniz. Progress bar örneğinde width yerine scaleX kullanmamın sebebi bu.
Ne zaman hâlâ JavaScript gerekir?
Scroll-driven animations her senaryoyu karşılamıyor. Scroll pozisyonuna göre veri fetch etmek, analytics event göndermek, karmaşık state değişikliği yapmak gibi işler hâlâ IntersectionObserver veya scroll event listener gerektirir. Ayrıca animasyonu kullanıcı etkileşimine göre durdurmak, geri almak ya da farklı bir keyframe'e atlamak istiyorsanız Web Animations API ile animation.currentTime'ı programatik olarak kontrol etmeniz gerekebilir.
CSS scroll-driven animations'ın doğru kullanım alanı görsel geri bildirim: progress bar, parallax, reveal animasyonları, sticky header'da gölge ekleme. Bu sınırlar içinde kalırsanız JavaScript bağımlılığını tamamen sıfırlayabilirsiniz. Sınırı aştığınız an, JS'e dönmekten çekinmeyin; API'yi zorlayarak karmaşık hack'ler üretmek, kazandığınız performans avantajını harcamanın en hızlı yolu.