Mert Özen Her Satırda Daha İleri
Frontend CSS3: Flexbox, Grid ve Modern CSS — #14

Scroll-driven Animation'lar ve View Transition'lar

Mert Özen 14 Kas 2026 16 dk 1 görüntülenme
Scroll-driven Animation'lar ve View Transition'lar

animation-timeline ile scroll() ve view() animasyonun ilerlemesini zamandan değil scroll konumundan alır ve Ekim 2026'dan beri Baseline; view transition'lar ise bir DOM değişikliğini iki görüntü arasında canlandırır. Yazı Bookstore'a okuma ilerleme çubuğu, ekrana girerken beliren kartlar, sıralama animasyonu, Angular'da withViewTransitions() ve Firefox'ta olmayan cross-document geçiş ekliyor.

Sürüm notu: Bu yazı CSS Scroll-driven Animations Level 1 ve CSS View Transitions Level 1/2 spesifikasyonlarına ve Baseline'ın Kasım 2026 durumuna göre yazıldı; örnekler Chrome 158 ve WebKit 27.2 (Playwright 1.65), Angular 22.2 ve TypeScript 6.0 ile denenmiştir, Firefox desteği web-features 3.46 verisinden ve web.dev'in aylık "New to the web platform" yazılarından alınmıştır.

Önce Tahmin Et

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı) için katalogdaki "Book of the week" kartında editörün notu bir okuma rehberine bağlanıyordu ama sayfa yoktu. Bu yazıda o sayfayı ekliyoruz ve en üstüne, sayfanın ne kadarının okunduğunu gösteren ince bir çubuk koyuyoruz. Çubuk JavaScript'le değil, scroll konumuna bağlı bir CSS animasyonuyla dolacak. Bir ekip arkadaşımız ilk denemeyi şöyle yazmış:

.reading-progress {
    animation-timeline: scroll(root block);
    animation: reading-progress linear both;
}

reading-progress keyframe'i çubuğun scale'ini 0'dan 1'e götürüyor. Sayfa en üstteyken, hiç scroll yapılmamışken çubuk ne kadar dolu görünür?

Bir önceki yazıda zamana bağlı animasyonları gördük. Bu yazıda animasyonun ilerlemesini zamandan alıp scroll konumuna bağlayan animation-timeline'a ve bir DOM (Document Object Model; sayfanın tarayıcıdaki element ağacı) değişikliğini iki ekran görüntüsü arasında canlandıran view transition'lara bakıyoruz: aynı sayfada liste sıralaması, Angular router'da sayfa geçişi ve çok sayfalı uygulamalarda sayfadan sayfaya geçiş.

Scroll Timeline: scroll() ve Okuma İlerleme Çubuğu

Normal bir animasyonun ilerlemesi zamandan gelir: 0 saniyede 0, süre bitince 1. animation-timeline bu kaynağı değiştirir. scroll() bir scroll container'ın konumunu kullanır: en üstte 0, en altta 1. view() ise bir element'in scroll container içinde görünür alandan geçişini kullanır. Farkı görmek için lab'de 200px yüksekliğinde, 700px içerikli küçük bir scroller kurdum ve içine 300. piksele 100px'lik bir "kart" koydum. Animasyonlar kayıtlı bir custom property'yi (--progress) 0'dan 1'e götürüyor; böylece ilerlemeyi doğrudan sayı olarak okuyabiliyorum:

@property --progress { syntax: "<number>"; inherits: false; initial-value: 0; }
@keyframes progress { to { --progress: 1; } }

/* 700px içerikli 200px'lik bir scroller: 0'dan 500px'e kadar scroll eder */
.scroller { block-size: 200px; overflow-y: scroll; }
.spacer { block-size: 300px; }

/* 1) scroll(): en yakın scroll container'ın ilerlemesi, en üstte 0, en altta 1 */
.scroll-progress { animation: progress linear both; animation-timeline: scroll(nearest); }

/* 2) view(): 100px'lik bir element'in scroller'dan geçişinin ilerlemesi, isimli aralıklara bölünmüş */
.subject { block-size: 100px; view-timeline: --book block; }
.range { animation: progress linear both; animation-timeline: --book; }
#cover { animation-range: cover; }
#entry { animation-range: entry; }
#contain { animation-range: contain; }
#exit { animation-range: exit; }

/* 3) timeline'dan sonra gelen kısaltma: animation-timeline auto'ya döner */
.wrong-order { animation-timeline: scroll(nearest); animation: progress linear both; }
node scripts/14-scroll.mjs
--- chromium
scrollTop  scroll()  cover  entry  contain  exit
    0px     0.00   0.00   0.00   0.00   0.00
  100px     0.20   0.00   0.00   0.00   0.00
  150px     0.30   0.17   0.50   0.00   0.00
  200px     0.40   0.33   1.00   0.00   0.00
  250px     0.50   0.50   1.00   0.50   0.00
  300px     0.60   0.67   1.00   1.00   0.00
  350px     0.70   0.83   1.00   1.00   0.50
  400px     0.80   1.00   1.00   1.00   1.00
  500px     1.00   1.00   1.00   1.00   1.00
timeline before the shorthand: auto, --progress 1.00
guide.html, 888px to scroll: 0% 0 1 | 25% 0.25 1 | 50% 0.5 1 | 100% 1
"The Hobbit" 0% in: opacity 0.25, translate y 24.1px | 50% in: opacity 0.75, translate y 8.0px | 100% in: opacity 1.00, translate y NaNpx (no-preference)
"The Hobbit" 0% in: opacity 0.00, translate y none | 50% in: opacity 0.50, translate y none | 100% in: opacity 1.00, translate y none (reduce)
--- webkit: 
scrollTop  scroll()  cover  entry  contain  exit
    0px     0.00   0.00   0.00   0.00   0.00
  100px     0.20   0.00   0.00   0.00   0.00
  150px     0.30   0.17   0.50   0.00   0.00
  200px     0.40   0.33   1.00   0.00   0.00
  250px     0.50   0.50   1.00   0.50   0.00
  300px     0.60   0.67   1.00   1.00   0.00
  350px     0.70   0.83   1.00   1.00   0.50
  400px     0.80   1.00   1.00   1.00   1.00
  500px     1.00   1.00   1.00   1.00   1.00
timeline before the shorthand: auto, --progress 1.00
guide.html, 888px to scroll: 0% 0 1 | 25% 0.25 1 | 50% 0.5 1 | 100% 1
"The Hobbit" 0% in: opacity 0.00, translate y 32.0px | 50% in: opacity 0.50, translate y 15.9px | 100% in: opacity 1.00, translate y 0.0px (no-preference)
"The Hobbit" 0% in: opacity 0.00, translate y none | 50% in: opacity 0.50, translate y none | 100% in: opacity 1.00, translate y none (reduce)

Tablonun scroll() kolonu basit: scroller 0'dan 500px'e kadar scroll edebiliyor ve ilerleme scrollTop / 500. 250px'de 0.50, en altta 1.00. Animasyonun süresi yok; animation kısaltmasında süre vermedim, çünkü scroll'a bağlı bir animasyonda süre anlamını kaybediyor. Kullanıcı scroll'u geri çektiğinde animasyon da geri gidiyor.

Tablonun altındaki satır tahmin sorusunun cevabı: animation-timeline'ı kısaltmadan önce yazan element'in timeline'ı auto, yani belgenin normal zaman çizelgesi, ve ilerlemesi en baştan 1.00. animation kısaltması animation-timeline'ı içermiyor ama sıfırlıyor: kısaltma, yazmadığın bütün longhand'leri başlangıç değerine çeker. Timeline auto'ya dönünce süresiz animasyon zamanla oynatılıyor, sıfır sürede bitiyor ve both fill-mode'u yüzünden son keyframe'de kalıyor. Okuma çubuğu sayfa açılır açılmaz tam dolu görünüyor. Kural: animation-timeline her zaman animation kısaltmasından sonra.

Bookstore'un okuma rehberi sayfasının en başına bir çubuk ekledim (<div class="reading-progress" aria-hidden="true">; görsel bir gösterge olduğu için ekran okuyuculardan gizli) ve stilini yeni bir component dosyasına yazdım:

.guide-page {
    max-inline-size: 40rem;
    margin-inline: auto;
    padding-inline: var(--space);
}

/* okuma ilerleme çubuğu: varsayılan olarak gizli, destekleyen tarayıcıda sayfanın scroll konumuyla dolar */
.reading-progress {
    position: fixed;
    inset-block-start: 0;
    inset-inline: 0;
    z-index: 10;
    block-size: 0.25rem;
    background-color: var(--color-accent);
    transform-origin: left;
    scale: 0 1;
}

@supports (animation-timeline: scroll()) {
    .reading-progress {
        /* kısaltma animation-timeline'ı sıfırlar, bu yüzden timeline ondan sonra gelir */
        animation: reading-progress linear both;
        animation-timeline: scroll(root block);
    }
}

@keyframes reading-progress {
    to {
        scale: 1 1;
    }
}

Çıktıda guide.html satırı 360px genişliğinde bir telefon ekranında ölçüldü: sayfa 888px scroll ediyor, çubuğun yatay ölçeği yolun %25'inde 0.25, yarısında 0.5, sonunda 1. scroll(root block) belgenin kendi scroll'unu, dikey eksende kullanıyor. Animasyonu @supports içine aldım ve çubuğun varsayılan ölçeğini 0 yaptım: scroll-driven animation'ı desteklemeyen bir tarayıcıda çubuk hiç görünmüyor. Yarım kalmış ya da hep dolu bir çubuk göstermektense hiç göstermemek daha doğru.

Bu işi JavaScript'le yapmanın klasik yolu bir scroll event listener'ı: her scroll event'inde konumu okuyup çubuğun stilini değiştirmek. Chrome ekibinin scroll-driven animation rehberi bunun iki sorununu anlatıyor: tarayıcı scroll'u ayrı bir süreçte yaptığı için event'ler asenkron geliyor ve ana thread'de çalışan bir animasyon, thread meşgulken takılıyor. Scroll timeline ise Web Animations ve CSS animasyonlarıyla aynı altyapıyı kullanıyor; transform ve opacity gibi property'lerde bir önceki yazıda ölçtüğümüz gibi ana thread'e uğramadan çalışabiliyor. Script'te scroll'dan sonra iki kare bekleyip okudum, çünkü timeline değeri bir sonraki karede güncelleniyor.

View Timeline: view() ve animation-range

view() bir element'in kendi görünürlüğünü timeline yapar. Lab'deki kart 100px yüksekliğinde ve 200px'lik pencereden geçiyor; view-timeline: --book block bu geçişe bir isim veriyor, içerideki göstergeler de animation-range ile geçişin farklı bölümlerine bağlı:

14-scroll-timelines-tr.png

Tablodaki değerler görseldeki çubuklarla birebir: cover, kartın görünmeye başladığı andan (scrollTop 100px, kartın üst kenarı pencerenin alt kenarında) tamamen çıktığı ana (400px) kadar. entry girişin kendisi (100–200px), contain kartın tamamen göründüğü aralık (200–300px), exit çıkış (300–400px). view()'ın varsayılan aralığı cover. Aralıkların ikisini birleştirmek de mümkün: animation-range: entry 0% entry 100% girişin tamamı, entry 50% cover 50% girişin yarısından kartın pencerenin ortasına gelişine kadar.

Ekrana Girerken Beliren Kitap Kartları

Bookstore'un katalog listesinde kartlar ekrana girerken belirsin istiyoruz. Bu efekt için önceden genellikle IntersectionObserver ile bir class eklenirdi; şimdi tek bir CSS bloğu yetiyor. Kuralı book-card.css'in sonuna ekledim:

/* kartlar ekrana girerken belirir; kayma yalnız daha az hareket istemeyen kullanıcılar için */
@supports (animation-timeline: view()) {
    .book-list > .book-card {
        animation: card-fade linear both;
        animation-timeline: view();
        animation-range: entry 0% entry 100%;

        @media (prefers-reduced-motion: no-preference) {
            animation-name: card-slide;
        }
    }
}

@keyframes card-fade {
    from {
        opacity: 0;
    }
}

@keyframes card-slide {
    from {
        opacity: 0;
        translate: 0 2rem;
    }
}

Animasyon girişin tamamına (entry 0% entry 100%) yayılıyor: kart pencerenin altından görünmeye başladığında opacity 0, tamamen girdiğinde 1. Çıktıdaki son iki satır 360x640'lık bir ekranda listenin son kartı, The Hobbit: yarısı girmişken opacity 0.50 ve 2rem'lik kaymanın yarısında (15.9px), tamamen girince opacity 1 ve kayma bitmiş. Animasyonda to keyframe'i yok; eksik keyframe element'in kendi değerini kullanıyor, yani kart animasyon bitince her zamanki haliyle duruyor.

Bir önceki yazıdaki kararı burada da uyguladım: kayma (card-slide) yalnız hareket tercihi bilinen kullanıcılar için; prefers-reduced-motion: reduce olan kullanıcıda kart yerinde beliriyor (card-fade). @media'yı kuralın içine nested yazmak, iki animasyon adını aynı yerde tutuyor. Çıktıdaki "reduce" satırında translate none, opacity yine 0'dan 1'e gidiyor.

Bir dikkat noktası: bu animasyon yalnız .book-list'teki kartlarda. Sayfa açıldığında tamamen ekranda olan kartlar view timeline'da girişini çoktan tamamlamış sayılıyor ve hemen tam görünüyor; ekranın alt kenarında yarısı görünen bir kart ise yarı saydam başlıyor ve scroll edildikçe netleşiyor. Ama bir kartın görünmesini scroll'a bağlamak, kullanıcı hiç scroll etmezse bazı içeriğin hiç görünmemesi riskini taşır. Opacity'yi 0'dan başlatan her scroll animasyonunda, içeriği desteklemeyen tarayıcıda ve ilk ekranda görünür bırakan bir varsayılan olduğundan emin ol; burada bunu @supports ve eksik to keyframe'i sağlıyor.

Same-document View Transition: Katalog Sıralaması

View transition, sayfanın iki halini ekran görüntüsü olarak yakalayıp aralarında animasyon yapan bir API. document.startViewTransition(callback) önce sayfanın eski halinin görüntüsünü alır, callback'i çalıştırıp DOM'u günceller, sonra yeni halin görüntüsünü alır ve ikisi arasında varsayılan olarak bir cross-fade yapar. view-transition-name verilen her element ayrı bir grup olarak yakalanır ve eski konumundan yeni konumuna taşınır. Bookstore'un katalog araç çubuğundaki sıralama seçimini buna bağladım:

// Kitap listesini yerinde sıralar; view transition olan tarayıcıda her kart eski yerinden yenisine gider.
const list = document.querySelector('.book-list');
const select = document.querySelector('.catalog-toolbar select');

const title = card => card.querySelector('.book-title').textContent;
const price = card => parseFloat(card.querySelector('.price').textContent);

function sortBooks() {
    const cards = [...list.children].sort((a, b) =>
        select.value === 'Price' ? price(a) - price(b) : title(a).localeCompare(title(b), 'en'));
    list.append(...cards);
}

select.addEventListener('change', () => {
    // eski tarayıcılar: animasyonsuz sırala
    if (!document.startViewTransition) {
        sortBooks();
        return;
    }
    document.startViewTransition(sortBooks);
});
/* cross-document view transition: ikisi de katılan iki sayfa arasındaki aynı origin'li navigasyon cross-fade ile geçer */
@view-transition {
    navigation: auto;
}

/* listedeki her kart ayrı bir view transition grubu; sıralama kartları yeni yerlerine taşır */
.book-list > .book-card {
    view-transition-name: match-element;
    view-transition-class: book-card;
}

::view-transition-group(*.book-card) {
    animation-duration: 300ms;
}

/* daha az hareket: kartlar yol almaz, yeni yerlerinde cross-fade eder */
@media (prefers-reduced-motion: reduce) {
    ::view-transition-group(*.book-card) {
        animation-duration: 0s;
    }
}

Her karta tek tek isim vermek yerine view-transition-name: match-element kullandım: tarayıcı element'in kimliğine göre kendisi bir isim üretiyor ve aynı element DOM'da yer değiştirdiğinde eski ve yeni görüntüsü eşleşiyor. view-transition-class: book-card ise bütün kartlara ortak bir class veriyor, böylece ::view-transition-group(*.book-card) ile hepsinin animasyonunu tek kuralda ayarlıyorum. İkisi de Ekim 2025'ten beri Baseline.

node scripts/14-view-transitions.mjs
--- chromium
sort no-preference: Dune, Middlemarch, Emma, The Hobbit -> Emma, The Hobbit, Middlemarch, Dune
  groups: 300ms (-520, 0) 300ms (-520, 0) 300ms (260, 0) 300ms (780, 0) 250ms (0, 0)
sort reduce: Dune, Middlemarch, Emma, The Hobbit -> Emma, The Hobbit, Middlemarch, Dune
  groups: 250ms (0, 0) 0ms (-520, 0) 0ms (-520, 0) 0ms (260, 0) 0ms (780, 0)
duplicate name: InvalidStateError: Transition was aborted because of invalid state. Snapshot capture failed | update ran: yes
book.html revealed with a view transition
--- webkit
sort no-preference: Dune, Middlemarch, Emma, The Hobbit -> Emma, The Hobbit, Middlemarch, Dune
  groups: 300ms (-520, 0) 300ms (-520, 0) 300ms (260, 0) 300ms (780, 0) 250ms (0, 0)
sort reduce: Dune, Middlemarch, Emma, The Hobbit -> Emma, The Hobbit, Middlemarch, Dune
  groups: 250ms (0, 0) 0ms (-520, 0) 0ms (-520, 0) 0ms (260, 0) 0ms (780, 0)
duplicate name: InvalidStateError: Multiple elements found with view-transition-name: book-cover | update ran: yes
book.html revealed with a view transition

1280px'lik ekranda dört kart tek satırda. Fiyata göre sıralayınca Emma ve The Hobbit 520 piksel sola, Middlemarch 260 piksel sağa, Dune 780 piksel sağa kayıyor; her kartın grubu 300ms. Listedeki 250ms'lik grup sayfanın kök görüntüsü (root), varsayılan cross-fade. "reduce" satırında kartların grupları 0ms: kartlar yeni yerlerine atlıyor ve eski ile yeni görüntü orada cross-fade ediyor. Hareket yok, değişiklik yine görünür.

startViewTransition'ı desteklemeyen tarayıcı için sort.js listeyi doğrudan sıralıyor. View transition'lar Ekim 2025'te Firefox 144 ile Baseline oldu; eski tarayıcılarda bu satırlar sayfanın çalışmasını değil, yalnız geçişin görünüşünü değiştiriyor.

Angular Router'da withViewTransitions()

Single page application'larda sayfa geçişi de bir DOM güncellemesi, bu yüzden aynı API çalışıyor. Angular router bunu bir provider olarak sunuyor. Serinin container query yazısında kurduğumuz bookstore-web projesine iki route ekledim: katalog ve kitap detayı:

import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideRouter, withComponentInputBinding, withViewTransitions } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideBrowserGlobalErrorListeners(),
    // her router navigasyonu document.startViewTransition() içinde çalışır; tarayıcı desteklemiyorsa navigasyon her zamanki gibi olur
    provideRouter(routes, withComponentInputBinding(), withViewTransitions()),
  ],
};

withViewTransitions() her router navigasyonunu document.startViewTransition() içinde çalıştırıyor; tarayıcı desteklemiyorsa navigasyon her zamanki gibi devam ediyor. Angular dokümanında hâlâ developer preview olarak işaretli. Kart component'indeki kapak görseline ve detay sayfasındaki büyük kapağa aynı isim verdim:

<article class="card">
  <img [src]="cover()" [alt]="'Cover of ' + bookTitle()" width="120" height="180"
       [style.view-transition-name]="'cover-' + slug()">
  <h3><a [routerLink]="['/books', slug()]">{{ bookTitle() }}</a></h3>
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-book-page',
  template: `
    <!-- karttaki kapakla aynı view-transition-name: tarayıcı bir görüntüyü diğerinin yerine taşır -->
    <img [src]="slug() + '.svg'" [alt]="'Cover of ' + slug()" width="240" height="360"
         [style.view-transition-name]="'cover-' + slug()">
    <h1>{{ slug() }}</h1>
  `,
  styles: `
    :host {
      display: block;
      padding: 1rem;
    }
  `,
})
export class BookPage {
  // withComponentInputBinding() ile :slug route parametresinden doldurulur
  readonly slug = input.required<string>();
}
cd angular
npm install
npx ng build
cd ..
node scripts/14-angular.mjs
--- chromium no-preference, /books/dune
  ::view-transition-group(cover-dune) 250ms 120px x 180px -> 240px x 360px
  ::view-transition-group(root) 250ms 1024px x 600px -> 1024px x 600px
--- chromium reduce, /books/dune
  ::view-transition-group(cover-dune) 0ms 120px x 180px -> 240px x 360px
  ::view-transition-group(root) 0ms 1024px x 600px -> 1024px x 600px
--- webkit no-preference, /books/dune
  ::view-transition-group(cover-dune) 250ms 120px x 180px -> 240px x 360px
  ::view-transition-group(root) 250ms 1024px x 600px -> 1024px x 600px
--- webkit reduce, /books/dune
  ::view-transition-group(cover-dune) 0ms 120px x 180px -> 240px x 360px
  ::view-transition-group(root) 0ms 1024px x 600px -> 1024px x 600px

Dune'un başlığına tıklayınca /books/dune'a geçiliyor ve iki grup oluşuyor: sayfanın kökü ve cover-dune. Kapağın grubu 120x180'den 240x360'a büyüyor: kullanıcı küçük kapağın büyüyüp detay sayfasındaki yerine gittiğini görüyor. İsmin her kitap için farklı olması şart; Emma ve Dune'un kapakları aynı sayfada ve aynı ismi alsalar geçiş hiç çalışmazdı (sebebi Sık Düşülen Tuzaklar'da).

Reduce satırlarında iki grup da 0ms. Bu kuralı component'in stillerine değil, global styles.css'e yazdım:

/* ::view-transition-* pseudo-element'leri belgenin köküne bağlıdır, hiçbir component'in içinde değildir:
   onlar için kurallar bir component'in stillerine değil, bu global dosyaya yazılır */

/* daha az hareket: kapak yeni yerine gitmez, iki sayfa yalnız cross-fade eder */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*) {
    animation-duration: 0s;
  }
}

Önce kuralı kart component'inin styles'ına koymayı denedim ve reduce'ta süre 250ms kaldı. Build çıktısına baktığımda sebebi gördüm: Angular'ın emulated encapsulation'ı kuralı [_ngcontent-%COMP%]::view-transition-group(*)'a çevirmişti. View transition pseudo-element'leri belgenin kök element'ine bağlı ve hiçbir component'in attribute'unu taşımıyor, bu yüzden bu selector hiçbir şeyle eşleşmiyor. Serinin nesting ve @scope yazısında gördüğümüz encapsulation burada ters tepiyor.

Cross-document View Transition: MPA ve ASP.NET Core MVC

Bookstore'un statik sayfaları, ASP.NET Core MVC (Model-View-Controller; veriyi, görünümü ve isteği karşılayan kodu ayıran yapı) ile yazılmış bir site gibi, her bağlantıda yeni bir belge yüklüyor. Bu tür çok sayfalı uygulamalar (MPA, Multi-Page Application; her sayfası ayrı belge olan uygulama) için JavaScript gerekmiyor: iki sayfa da CSS'te @view-transition { navigation: auto; } diyorsa, aynı origin'deki bir navigasyon eski sayfadan yenisine cross-fade ile geçiyor. Kural view-transitions.css'in en başında ve main.css her sayfada olduğu için bütün site katılıyor. Bir ASP.NET Core MVC uygulamasında aynı kuralı layout'un yüklediği stil dosyasına yazmak yeterli.

Ölçüm için gerçek bir origin gerekiyor; bu yüzden scripts/site.mjs web/ klasörünü Playwright'ın page.route'u ile http://bookstore.test/ altında sunuyor. Çıktıdaki "book.html revealed with a view transition" satırı yeni sayfanın pagereveal event'inden geliyor: event'in viewTransition'ı dolu, yani sayfa bir geçişle açıldı. İki motorda da çalıştı.

Ama bu özellik Baseline değil: web-features'a göre cross-document view transition Chrome 126 ve Safari 18.2'de var, Firefox'ta yok. Interop 2026'nın view transition odak alanı cross-document geçişleri de kapsıyor, yani tarayıcılar bu yıl üzerinde çalışıyor. Bugün için doğru yaklaşım progressive enhancement: kural desteklemeyen tarayıcıda hiçbir şey yapmıyor ve navigasyon her zamanki gibi oluyor. Bir şeyi bozma ihtimali olmadığı için Firefox'u beklemeden eklenebilir.

Üretimde Dikkat

Scroll-driven animation'lar Ekim 2026'nın sonunda Firefox 159 ile Baseline oldu; newly available, yani bundan önceki Firefox sürümlerinde yok. Bu yüzden Bookstore'daki iki kullanım da @supports (animation-timeline: …) içinde ve ikisinde de desteklemeyen tarayıcı tam ve doğru bir sayfa görüyor: okuma çubuğu yok, kartlar animasyonsuz. Same-document view transition'lar Ekim 2025'ten beri Baseline; Angular'ın entegrasyonu ve cross-document geçişler için yukarıdaki notlar geçerli. Scroll-driven animation'lar ve view transition'lar Interop 2026'nın odak alanları arasında; tarayıcılar arasındaki küçük farkların bu yıl giderilmesi bekleniyor.

View transition sırasında kullanıcı sayfanın kendisini değil, eski ve yeni halinin görüntülerini görür. 250–300ms'lik bir geçişte bu fark edilmez, ama uzun bir geçiş arayüzün yavaş olduğu hissini verir. Geçişleri kısa tut ve geçişin süresini, beklemeyi gizlemek için uzatma.

Sık Düşülen Tuzaklar

Birincisi, animation-timeline'ı animation kısaltmasından önce yazmak. Ölçtüğümüz gibi kısaltma onu auto'ya çeker ve animasyon zamanla, sıfır sürede oynar.

İkincisi, aynı view-transition-name'i iki element'e vermek. Çıktıdaki "duplicate name" satırında iki motor da InvalidStateError verdi; Chromium'un mesajı yalnız "Snapshot capture failed" diyor, WebKit hangi ismin çakıştığını söylüyor. DOM güncellemesi yine yapılıyor, yalnız geçişin tamamı atlanıyor; bir listede tek bir isim çakışması bütün animasyonu sessizce yok ediyor. Listelerde match-element ya da veriden üretilmiş benzersiz isimler kullan.

Üçüncüsü, view transition pseudo-element'leri için kuralları Angular'da bir component'in stillerine yazmak. Encapsulation selector'e attribute ekler ve kural hiçbir şeyle eşleşmez; bu kurallar global stil dosyasına aittir.

Dördüncüsü, reduced-motion'ı unutmak. View transition'lar ve scroll animasyonları ekranın büyük kısmını hareket ettirebilir; hareketi azaltmak isteyen kullanıcı için kaymaları kaldır, fade'leri bırak.

Küçük Bir Deneme

★☆☆ Lab'deki #entry göstergesinin aralığını entry 50% cover 50% yap. Tablonun hangi scrollTop değerinde 0 ve 1 olacağını tahmin et, sonra node scripts/14-scroll.mjs ile kontrol et.

★★☆ Okuma rehberi sayfasının her h2'sine, ekranın üst üçte birine geldiğinde rengini accent'e çeviren bir view timeline animasyonu ekle. Hangi animation-range'in doğru olduğunu ölçerek bul ve reduce kullanıcısında da çalışıp çalışmaması gerektiğine karar ver.

★★★ Katalogdaki "Book of the week" kartı ile book.html'deki kapak arasında cross-document bir geçiş kur: iki sayfada da kapağa view-transition-name: cover-dune ver. Listedeki Dune kartının da aynı ismi almaması için ne yapman gerektiğini düşün ve pagereveal event'inde cover-dune grubunun oluştuğunu bir script'le doğrula.

Bir sonraki yazıda konumlandırmaya geri dönüyoruz: anchor positioning. Bir element'i başka bir element'e göre CSS ile yerleştirmek, ekrana sığmadığında yön değiştirmesini sağlamak ve popover ile invoker commands'i birleştirerek Bookstore'a sıfır JavaScript'le bir kitap önizleme tooltip'i ve kullanıcı menüsü eklemek.