Tek sayfalık uygulamalar için aynı doküman görünümü geçişleri

Yayınlanma tarihi: 17 Ağustos 2021, Son güncelleme tarihi: 25 Eylül 2024

Tek bir dokümanda görüntü geçişi yapıldığında buna aynı doküman görüntü geçişi denir. Bu durum genellikle DOM'u güncellemek için JavaScript'in kullanıldığı tek sayfalık uygulamalarda (SPA'lar) görülür. Aynı doküman görünümü geçişleri, Chrome 111'den itibaren Chrome'da desteklenmektedir.

Aynı doküman görüntüleme geçişini tetiklemek için document.startViewTransition komutunu çağırın:

function handleClick(e) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow();
    return;
  }

  // With a View Transition:
  document.startViewTransition(() => updateTheDOMSomehow());
}

Tetiklendiğinde tarayıcı, üzerinde view-transition-name CSS mülkünün tanımlandığı tüm öğelerin anlık görüntülerini otomatik olarak yakalar.

Ardından, DOM'yi güncelleyen, iletilen geri çağırmayı yürütür ve ardından yeni durumun anlık görüntülerini alır.

Daha sonra bu anlık görüntüler, yapay öğelerden oluşan bir ağaçta düzenlenir ve CSS animasyonlarının gücü kullanılarak canlandırılır. Eski ve yeni durumdaki anlık görüntü çiftleri, eski konum ve boyutlarından yeni konumlarına sorunsuz bir şekilde geçiş yapar. Bu sırada içerikleri de yumuşak bir şekilde geçiş yapar. Dilerseniz animasyonlarınızı CSS kullanarak özelleştirebilirsiniz.


Varsayılan geçiş: Geçiş efekti

Varsayılan görünüm geçişi çapraz geçiştir; bu nedenle API'ye güzel bir giriş niteliğindedir:

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

Burada updateTheDOMSomehow, DOM'u yeni duruma değiştirir. Bunu istediğiniz gibi yapabilirsiniz. Örneğin, öğe ekleyebilir veya kaldırabilir, sınıf adlarını değiştirebilir ya da stilleri değiştirebilirsiniz.

Sayfalar böylece birbirine kademeli olarak kaybolur:

Varsayılan geçiş efekti. En az demo. Kaynak.

Tamam, geçiş çok etkileyici değil. Geçişler özelleştirilebilir. Ancak önce bu temel geçişin nasıl çalıştığını anlamanız gerekir.


Bu geçişlerin işleyiş şekli

Önceki kod örneğini güncelleyelim.

document.startViewTransition(() => updateTheDOMSomehow(data));

.startViewTransition() çağrıldığında, API sayfanın mevcut durumunu yakalar. Buna anlık görüntü almak da dahildir.

İşlem tamamlandığında .startViewTransition() adresine iletilen geri çağırma işlevi çağrılır. DOM burada değiştirilir. Ardından, API sayfanın yeni durumunu yakalar.

Yeni durum yakalandıktan sonra, API aşağıdaki gibi bir sözde öğe ağacı oluşturur:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

::view-transition, sayfadaki diğer her şeyin üzerinde bir yer paylaşımında yer alır. Bu seçenek, geçiş için bir arka plan rengi ayarlamak istiyorsanız kullanışlıdır.

::view-transition-old(root), eski görünümün ekran görüntüsüdür ve ::view-transition-new(root), yeni görünümün canlı temsilidir. Her ikisi de CSS "değiştirilen içerik" olarak oluşturulur (<img> gibi).

Eski görünüm opacity: 1'ten opacity: 0'e, yeni görünüm ise opacity: 0'ten opacity: 1'e animasyonlu olarak geçerek geçiş oluşturur.

Animasyonun tamamı CSS animasyonları kullanılarak gerçekleştirilir. Bu nedenle, CSS ile özelleştirilebilir.

Geçişi özelleştirme

Görünüm geçişi sözde öğelerinin tümü CSS ile hedeflenebiliyor. Animasyonlar CSS kullanılarak tanımlandığından, mevcut CSS animasyon özelliklerini kullanarak bunları değiştirebilirsiniz. Örneğin:

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

Bu tek değişiklikle, geçiş artık çok yavaş:

Uzun geçiş efekti. Minimal demo. Kaynak.

Tamam, yine de etkileyici değil. Bunun yerine aşağıdaki kod, Material Design'ın paylaşılan eksen geçişini uygular:

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

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes slide-from-right {
  from { transform: translateX(30px); }
}

@keyframes slide-to-left {
  to { transform: translateX(-30px); }
}

::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

Sonuç şöyle olur:

Paylaşılan eksen geçişi. En az demo. Kaynak.

Birden fazla öğenin geçişini yapma

Önceki demoda, paylaşılan eksen geçişinde sayfanın tamamı yer almaktadır. Bu, sayfanın büyük bir kısmı için işe yarar ancak başlık için pek uygun değil. Çünkü başlık kaydırıldığında tekrar geri kaydırılır.

Bu durumu önlemek için üstbilgiyi sayfanın geri kalanından ayıklayıp ayrı olarak animasyona ekleyebilirsiniz. Bu işlem, öğeye bir view-transition-name atayarak yapılır.

.main-header {
  view-transition-name: main-header;
}

view-transition-name değeri istediğiniz gibi olabilir (none dışında, geçiş adı yoktur). Öğeyi geçiş boyunca benzersiz bir şekilde tanımlamak için kullanılır.

Bunun sonucunda:

Sabit başlık içeren paylaşılan eksen geçişi. Minimal demo. Kaynak.

Şimdi başlık yerinde kalıyor ve çapraz geçiş yapıyor.

Bu CSS bildirimi, sözde öğe ağacının değişmesine neden oldu:

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
   └─ ::view-transition-image-pair(main-header)
      ├─ ::view-transition-old(main-header)
      └─ ::view-transition-new(main-header)

Artık iki geçiş grubu vardır. Biri başlık, diğeri ise geri kalanı içindir. Bunlar CSS ile bağımsız olarak hedeflenerek farklı geçişler verilebilir. Ancak bu durumda main-header, varsayılan geçiş olan geçiş efektiyle bırakıldı.

Tamam. Varsayılan geçiş yalnızca çapraz geçişten ibaret değildir. ::view-transition-group ürününde de geçiş yapılır:

  • Konumlandırma ve dönüştürme (transform kullanarak)
  • Genişlik
  • Boy

Başlık, DOM değişikliğinin her iki tarafında da aynı boyut ve konumda olduğundan bu durum şimdiye kadar önemli değildi. Ancak başlıktaki metni de çıkarabilirsiniz:

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

fit-content kullanılır. Böylece öğe, kalan genişliğe uzatmak yerine metnin boyutuyla kapsanır. Bu olmadan, geri oku her iki sayfada aynı boyuta sahip olmak yerine başlık metin öğesinin boyutunu küçültür.

Şimdi üzerinde çalışacağımız üç bölüm var:

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

Ancak yine de varsayılanları kullanacağız:

Kayan başlık metni. En az demo. Kaynak.

Başlık metni şimdi geri düğmesine yer açmak için kaydırılıyor.


view-transition-class ile birden fazla sözde öğeyi aynı şekilde animasyonlu hale getirme

Tarayıcı Desteği

  • Chrome: 125.
  • Kenar: 125.
  • Firefox: Desteklenmez.
  • Safari Teknoloji Önizlemesi: desteklenir.

Sayfada bir grup kartın yanı sıra bir başlık içeren bir görüntü geçişiniz olduğunu varsayalım. Başlık dışındaki tüm kartları hareket ettirmek için her kartı ayrı ayrı hedefleyen bir seçici yazmanız gerekir.

h1 {
    view-transition-name: title;
}
::view-transition-group(title) {
    animation-timing-function: ease-in-out;
}

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }

#card20 { view-transition-name: card20; }

::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),

::view-transition-group(card20) {
    animation-timing-function: var(--bounce);
}

20 öğeniz var mı? Bu, yazmanız gereken 20 seçici demektir. Yeni bir öğe mi ekliyorsunuz? Ardından, animasyon stillerini uygulayan seçiciyi büyütmeniz gerekir. Tam olarak ölçeklenebilir değildir.

view-transition-class, aynı stil kuralını uygulamak için görünüm geçişi sözde öğelerinde kullanılabilir.

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }

#card20 { view-transition-name: card20; }

#cards-wrapper > div {
  view-transition-class: card;
}
html::view-transition-group(.card) {
  animation-timing-function: var(--bounce);
}

Aşağıdaki kartlar örneği, önceki CSS snippet'inden yararlanır. Yeni eklenenler de dahil olmak üzere tüm kartlara aynı zamanlama tek bir seçiciyle uygulanır: html::view-transition-group(.card).

Kartlar demosunun kaydı. view-transition-class kullanıldığında, eklenen veya kaldırılan kartlar hariç tüm kartlara aynı animation-timing-function uygulanır.

Hata ayıklama geçişleri

Görünüm geçişleri CSS animasyonları temel alınarak oluşturulduğundan Chrome Geliştirici Araçları'ndaki Animasyonlar paneli, geçişlerde hata ayıklama için mükemmel bir seçenektir.

Animasyonlar panelini kullanarak bir sonraki animasyonu duraklatabilir, ardından animasyonda ileri geri oynatabilirsiniz. Bu sırada geçiş sözde öğelerini Öğeler panelinde bulabilirsiniz.

Chrome Geliştirici Araçları ile görünüm geçişlerinde hata ayıklama.

Geçiş öğelerinin aynı DOM öğesi olması gerekmez.

Başlık ve başlıktaki metin için ayrı geçiş öğeleri oluşturmak üzere şimdiye kadar view-transition-name kullandık. Bunlar, kavram olarak DOM değişikliğinden önce ve sonra aynı öğedir ancak böyle olmadığı durumlarda geçişler oluşturabilirsiniz.

Örneğin, yerleştirilmiş ana video öğesine view-transition-name verilebilir:

.full-embed {
  view-transition-name: full-embed;
}

Ardından, küçük resim tıklandığında yalnızca geçiş süresince aynı view-transition-name değeri verilebilir:

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

Sonuç:

Bir öğenin diğerine geçişi. Minimal demo. Kaynak.

Küçük resim, ana resme geçiş yapar. Kavramsal olarak (ve kelimenin tam anlamıyla) farklı öğeler olsalar da geçiş API'si, aynı view-transition-name öğesini paylaştıkları için bunları aynı öğe olarak değerlendirir.

Bu geçişin gerçek kodu, küçük resim sayfasına geri geçişi de ele aldığından önceki örnekten biraz daha karmaşıktır. Eksiksiz uygulama için kaynağa bakın.


Özel giriş ve çıkış geçişleri

Şu örneği inceleyin:

Kenar çubuğuna girme ve kenar çubuğundan çıkılıyor. Minimal demo. Kaynak.

Kenar çubuğu geçişin bir parçasıdır:

.sidebar {
  view-transition-name: sidebar;
}

Ancak önceki örnekteki üstbilginin aksine kenar çubuğu tüm sayfalarda görünmez. Her iki durumda da kenar çubuğu varsa geçiş sözde öğeleri şöyle görünür:

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

Bununla birlikte, kenar çubuğu yalnızca yeni sayfadaysa ::view-transition-old(sidebar) sözde öğesi orada bulunmaz. Kenar çubuğu için "eski" resim olmadığından resim çiftinde yalnızca ::view-transition-new(sidebar) bulunur. Benzer şekilde, kenar çubuğu yalnızca eski sayfadaysa resim çiftinde yalnızca bir ::view-transition-old(sidebar) olacaktır.

Önceki demoda, kenar çubuğu, gösterilmeye başlandığına, gösterilmeye son verildiğine veya her iki durumda da gösterildiğine bağlı olarak farklı geçişler yapıyor. Sağdan kaydırılarak ve yavaşça belirginleşerek girer, sağa kaydırılarak ve yavaşça kaybolarak çıkar ve her iki durumda da yerinde kalır.

Belirli giriş ve çıkış geçişleri oluşturmak için, resim çiftindeki tek alt öğe olduğunda eski veya yeni sözde öğeleri hedeflemek üzere :only-child sözde sınıfını kullanabilirsiniz:

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

Bu durumda, varsayılan ayar mükemmel olduğu için kenar çubuğu her iki durumda da mevcut olduğunda belirli bir geçiş yoktur.

Eş zamansız DOM güncellemeleri ve içerik bekleniyor

.startViewTransition() işlevine iletilen geri çağırma işlevi, bir promise döndürebilir. Bu promise, DOM'un eşzamansız olarak güncellenmesine ve önemli içeriğin hazır olmasını beklemeye olanak tanır.

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

Geçiş, taahhüt yerine getirilene kadar başlamaz. Bu süre zarfında sayfa donduğu için buradaki gecikmeler minimum düzeyde tutulmalıdır. Ağ getirme işlemleri, .startViewTransition() geri çağırma işleminin bir parçası olarak değil, sayfa hâlâ tamamen etkileşimli durumdayken .startViewTransition() çağrılmadan önce yapılmalıdır.

Resimlerin veya yazı tiplerinin hazır olmasını beklemeye karar verirseniz agresif bir zaman aşımı kullandığınızdan emin olun:

const wait = ms => new Promise(r => setTimeout(r, ms));

document.startViewTransition(async () => {
  updateTheDOMSomehow();

  // Pause for up to 100ms for fonts to be ready:
  await Promise.race([document.fonts.ready, wait(100)]);
});

Ancak bazı durumlarda gecikmeden tamamen kaçınmak ve sahip olduğunuz içeriği kullanmak daha iyidir.


Elinizdeki içeriklerden en iyi şekilde yararlanın

Küçük resim daha büyük bir resme geçtiğinde:

Daha büyük bir görsele geçiş yapan küçük resim. Demo sitesini deneyin.

Varsayılan geçiş çapraz geçiştir. Diğer bir deyişle, küçük resim henüz yüklenmemiş tam resimle çapraz geçiş yapabilir.

Bu sorunu çözmenin bir yolu, geçişi başlatmadan önce resmin tamamını yüklemesini beklemektir. İdeal olarak bu, .startViewTransition() çağrılmadan önce yapılır. Böylece sayfa etkileşimli kalır ve kullanıcıya öğelerin yüklendiğini göstermek için bir döner simge gösterilebilir. Ancak bu durumda daha iyi bir yol var:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
}

Artık küçük resim kaybolmaz, tam resmin altında kalır. Yani yeni görünüm yüklenmemişse küçük resim geçiş boyunca görünür. Bu, geçişin hemen başlayabileceği ve resmin tamamının kendi süresinde yükleneceği anlamına gelir.

Yeni görünümde şeffaflık olsa bu yöntem işe yaramazdı. Ancak bu durumda öyle olmadığını biliyoruz. Dolayısıyla bu optimizasyonu yapabiliriz.

En boy oranındaki değişiklikleri işleme

Şimdiye kadar yapılan tüm geçişler aynı en boy oranına sahip öğelere yapılmıştır ancak bu her zaman böyle olmaz. Küçük resim 1:1 ve ana resim 16:9 ise ne olur?

En boy oranı değiştikçe bir öğe diğerine geçiş yapıyor. En az demo. Kaynak.

Varsayılan geçişte grup, önceki boyuttan sonraki boyuta animasyonla geçer. Eski ve yeni görünümler, grubun %100 genişliğinde ve otomatik yüksekliktedir. Yani grup boyutundan bağımsız olarak en boy oranlarını korurlar.

Bu iyi bir varsayılan değerdir ancak bu durumda istenen bu değildir. Bu durumda:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
  /* Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. */
  height: 100%;
  /* Clip any overflow of the view */
  overflow: clip;
}

/* The old view is the thumbnail */
::view-transition-old(full-embed) {
  /* Maintain the aspect ratio of the view,
  by shrinking it to fit within the bounds of the element */
  object-fit: contain;
}

/* The new view is the full image */
::view-transition-new(full-embed) {
  /* Maintain the aspect ratio of the view,
  by growing it to cover the bounds of the element */
  object-fit: cover;
}

Yani genişlik genişledikçe küçük resim öğenin ortasında kalır ancak tam resim 1:1'den 16:9'a geçerken "kırpılmaz".

Daha ayrıntılı bilgi için Geçişleri görüntüleme: En boy oranı değişikliklerini işleme başlıklı makaleyi inceleyin.


Farklı cihaz durumlarında geçişleri değiştirmek için medya sorgularını kullanma

Mobilde ve masaüstünde farklı geçişler kullanmak isteyebilirsiniz. Örneğin, mobilde tam slaytın yan tarafından tam slayt gösterildiği, masaüstünde ise daha küçük bir slaytın gösterildiği şu örnekteki gibi:

Bir öğenin diğerine geçişi. Minimal demo. Kaynak.

Bu, normal medya sorguları kullanılarak yapılabilir:

/* Transitions for mobile */
::view-transition-old(root) {
  animation: 300ms ease-out both full-slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both full-slide-from-right;
}

@media (min-width: 500px) {
  /* Overrides for larger displays.
  This is the shared axis transition from earlier in the article. */
  ::view-transition-old(root) {
    animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
  }

  ::view-transition-new(root) {
    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
  }
}

Eşleşen medya sorgularına bağlı olarak view-transition-name atadığınız öğeleri değiştirmek de isteyebilirsiniz.


"Düşük hareket" tercihine tepki verme

Kullanıcılar, işletim sistemlerinde daha az hareketi tercih ettiklerini belirtebilir ve bu tercih CSS'de gösterilir.

Bu kullanıcıların geçişlerini önlemeyi seçebilirsiniz:

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

Ancak, "azaltılmış hareket" tercihi kullanıcının hareket görmek istemediği anlamına gelmez. Önceki snippet yerine, öğeler arasındaki ilişkiyi ve veri akışını ifade eden daha ince bir animasyon seçebilirsiniz.


Görüntüleme geçişi türleriyle birden fazla görüntüleme geçişi stilini işleme

Tarayıcı Desteği

  • Chrome: 125.
  • Kenar: 125..
  • Firefox: Desteklenmez.
  • Safari: 18.

Bazen belirli bir görünümden diğerine geçiş için özel olarak tasarlanmış bir geçiş gerekir. Örneğin, sayfalara ayırma sırasına göre sonraki veya önceki sayfaya giderken, dizideki daha üst veya daha alt bir sayfaya mı gittiğinize bağlı olarak içerikleri farklı bir yönde kaydırmak isteyebilirsiniz.

Sayfalandırma demosunun kaydı. Geçişler, hangi sayfaya gittiğinize bağlı olarak farklı olur.

Bunun için etkin bir görüntüleme geçişine bir veya daha fazla tür atamanıza olanak tanıyan görüntüleme geçişi türlerini kullanabilirsiniz. Örneğin, sayfalara ayırma sırasına göre daha üst bir sayfaya geçerken forwards türünü, daha alt bir sayfaya geçerken backwards türünü kullanın. Bu türler yalnızca bir geçiş yakalanırken veya gerçekleştirilirken etkindir ve her tür farklı animasyonlar kullanmak için CSS aracılığıyla özelleştirilebilir.

Aynı doküman görünümünde geçişte türleri kullanmak için types öğesini startViewTransition yöntemine iletirsiniz. Buna izin vermek için document.startViewTransition bir nesneyi de kabul eder: update DOM'yi güncelleyen geri çağırma işlevidir, types ise türlere sahip bir dizidir.

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});
.

Bu türlere yanıt vermek için :active-view-transition-type() seçicisini kullanın. Hedeflemek istediğiniz type öğesini seçiciye iletin. Bu sayede, bir tanesinin bildirimleri diğerinin bildirimlerini etkilemeden birden fazla görüntü geçişinin stillerini birbirinden ayrı tutabilirsiniz.

Türler yalnızca geçiş yakalanırken veya gerçekleştirilirken geçerli olduğundan, yalnızca bu türe sahip görüntüleme geçişinde bir öğede view-transition-name değerini ayarlamak (veya ayarını kaldırmak) için seçiciyi kullanabilirsiniz.

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only (using the default root snapshot) */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

Aşağıdaki sayfalandırma demosunda sayfa içerikleri, ziyaret ettiğiniz sayfa numarasına göre ileri veya geri kaydırılır. Türler, tıklama sırasında belirlenir ve document.startViewTransition öğesine iletilir.

Türünden bağımsız olarak tüm etkin görünüm geçişlerini hedeflemek için bunun yerine :active-view-transition sözde sınıf seçicisini kullanabilirsiniz.

html:active-view-transition {
    
}

Görünüm geçiş kökünde sınıf adı kullanarak birden fazla görünüm geçişi stilini işleme

Bazen belirli bir görüntüleme türünden diğerine geçiş için özel olarak tasarlanmış bir geçiş gerekir. Ya da 'geri' gezinme 'ileri' seçeneğinden farklı olmalıdır yardımcı olabilir.

"Geri" giderken farklı geçişler. Minimal demo. Kaynak.

Geçiş türleri kullanılmadan önce bu durumların üstesinden gelmenin yolu, geçiş kökünde geçici olarak bir sınıf adı ayarlamaktı. document.startViewTransition çağrılırken bu geçiş kökü, JavaScript'te document.documentElement kullanılarak erişilebilen <html> öğesidir:

if (isBackNavigation) {
  document.documentElement.classList.add('back-transition');
}

const transition = document.startViewTransition(() =>
  updateTheDOMSomehow(data)
);

try {
  await transition.finished;
} finally {
  document.documentElement.classList.remove('back-transition');
}

Bu örnekte, geçiş tamamlandıktan sonra sınıfları kaldırmak için transition.finished kullanılmıştır. Geçiş, bitiş durumuna ulaştığında sona erer. Bu nesnenin diğer özellikleri API referansında ele alınmıştır.

Artık bu sınıf adını CSS'nizde kullanarak geçişi değiştirebilirsiniz:

/* 'Forward' transitions */
::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
      cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
  animation-name: fade-out, slide-to-right;
}

.back-transition::view-transition-new(root) {
  animation-name: fade-in, slide-from-left;
}

Medya sorgularında olduğu gibi, bu sınıfların varlığı hangi öğelerin view-transition-name almasını değiştirmek için de kullanılabilir.


Diğer animasyonlar dondurulmadan geçişler yapma

Video geçiş konumunu gösteren bu demoya göz atın:

Video geçişi. Minimal demo. Kaynak.

Bir sorun gördünüz mü? Aksi halde merak etmeyin. Burada yavaşlayabilir:

Video geçişi, daha yavaş. En az demo. Kaynak.

Geçiş sırasında video donmuş gibi görünür, ardından videonun oynatma sürümü yavaşça açılır. Bunun nedeni, ::view-transition-old(video)'ün eski görünümün ekran görüntüsü, ::view-transition-new(video)'un ise yeni görünümün canlı görüntüsü olmasıdır.

Bunu düzeltebilirsiniz ama önce kendinize düzeltmenin gerekip gerekmediğini sorun. Geçiş normal hızda oynatıldığında "sorunu" görmediyseniz değiştirmenizi önermem.

Gerçekten düzeltmek istiyorsanız ::view-transition-old(video) simgesini göstermeyin, doğrudan ::view-transition-new(video) simgesine geçin. Bunu varsayılan stilleri ve animasyonları geçersiz kılarak yapabilirsiniz:

::view-transition-old(video) {
  /* Don't show the frozen old view */
  display: none;
}

::view-transition-new(video) {
  /* Don't fade the new view in */
  animation: none;
}

Hepsi bu kadar!

Video geçişi, daha yavaş. En az demo. Kaynak.

Artık video, geçiş boyunca oynatılır.


Gezinme API'si (ve diğer çerçeveler) ile entegrasyon

Görünüm geçişleri, diğer çerçevelerle veya kitaplıklarla entegre edilebilecek şekilde belirtilir. Örneğin, tek sayfalık uygulamanız (SPA) bir yönlendirici kullanıyorsa, görüntüleme geçişi kullanarak içeriği güncellemek için yönlendiricinin güncelleme mekanizmasını ayarlayabilirsiniz.

Bu sayfalara ayırma demosundan alınan aşağıdaki kod snippet'inde, görüntüleme geçişleri desteklendiğinde Gezinme API'sinin müdahale işleyicisi document.startViewTransition çağrısı yapacak şekilde ayarlanmıştır.

navigation.addEventListener("navigate", (e) => {
    // Don't intercept if not needed
    if (shouldNotIntercept(e)) return;

    // Intercept the navigation
    e.intercept({
        handler: async () => {
            // Fetch the new content
            const newContent = await fetchNewContent(e.destination.url, {
                signal: e.signal,
            });

            // The UA does not support View Transitions, or the UA
            // already provided a Visual Transition by itself (e.g. swipe back).
            // In either case, update the DOM directly
            if (!document.startViewTransition || e.hasUAVisualTransition) {
                setContent(newContent);
                return;
            }

            // Update the content using a View Transition
            const t = document.startViewTransition(() => {
                setContent(newContent);
            });
        }
    });
});

Hepsi olmasa da bazı tarayıcılar, kullanıcı gezinmek için kaydırma hareketi yaptığında kendi geçişini sağlar. Bu durumda, olumsuz veya kafa karıştırıcı bir kullanıcı deneyimine yol açacağından kendi görünüm geçişinizi tetiklememelisiniz. Kullanıcı, biri tarayıcı tarafından diğeri sizin tarafınızdan sağlanan iki geçişin art arda çalıştığını görür.

Bu nedenle, tarayıcı kendi görsel geçişini sağladığında bir görünüm geçişinin başlamasının engellenmesi önerilir. Bunu yapmak için NavigateEvent örneğinin hasUAVisualTransition mülkünün değerini kontrol edin. Tarayıcı bir görsel geçiş sağladığında özellik true olarak ayarlanır. Bu hasUIVisualTransition özelliği PopStateEvent örnekte de mevcut.

Önceki snippet'te, görünüm geçişinin çalıştırılıp çalıştırılmayacağını belirleyen kontrolde bu mülk dikkate alınır. Aynı doküman görünümü geçişleri için destek yoksa veya tarayıcı kendi geçişini zaten sağlıyorsa görüntüleme geçişi atlanır.

if (!document.startViewTransition || e.hasUAVisualTransition) {
  setContent(newContent);
  return;
}

Aşağıdaki kayıtta, kullanıcı önceki sayfaya geri gitmek için ekranı kaydırmaktadır. Soldaki yakalamada hasUAVisualTransition işareti yoktur. Sağdaki kayıtta onay işareti yer alır. Bu nedenle, tarayıcı görsel bir geçiş sağladığı için manuel görüntüleme geçişi atlanır.

hasUAVisualTransition için kontrol olmadan (solda) ve genişlik (sağda) aynı sitenin karşılaştırması
etkinleştirildiğinde kullanılabilir.

JavaScript ile Animasyon Oluşturma

Şimdiye kadar tüm geçişler CSS kullanılarak tanımlandı ancak bazen CSS yeterli değildir:

Daire geçişi. En az demo. Kaynak.

Bu geçişin bazı bölümleri, tek başına CSS ile gerçekleştirilemez:

  • Animasyon, tıklama konumundan başlar.
  • Animasyon, dairenin en uzak köşeye kadar yarıçapında olmasıyla sona erer. Yine de bunun ileride CSS ile mümkün olacağını umuyoruz.

Neyse ki Web Animasyonu API'sini kullanarak geçişler oluşturabilirsiniz.

let lastClick;
addEventListener('click', event => (lastClick = event));

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // With a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

Bu örnekte, geçiş sözde öğeleri başarıyla oluşturulduktan sonra çözümlenen bir taahhüt olan transition.ready kullanılmaktadır. Bu nesnenin diğer özellikleri API referansında ele alınmıştır.


Geçişler bir iyileştirme olarak

View Transition API, bir DOM değişikliğini "sarmalamak" ve bunun için bir geçiş oluşturmak üzere tasarlanmıştır. Ancak bu geçiş, uygulamanız "hata" almamalıdır. Bunun gibi, bir iyileştirme olarak değerlendirilmelidir. durumu gösterir. İdeal olarak geçiş başarısız olmamalıdır ancak başarısız olursa kullanıcı deneyiminin geri kalanı etkilenmemelidir.

Geçişleri bir geliştirme olarak ele almak için geçiş taahhütlerini, geçiş başarısız olursa uygulamanızın hata vermesine neden olacak şekilde kullanmamaya dikkat edin.

Yapılmaması gerekenler:
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  await transition.ready;

  document.documentElement.animate(
    {
      clipPath: [`inset(50%)`, `inset(0)`],
    },
    {
      duration: 500,
      easing: 'ease-in',
      pseudoElement: '::view-transition-new(root)',
    }
  );
}

Bu örnekteki sorun, geçiş ready durumuna ulaşamazsa switchView()'ün reddetmesidir. Ancak bu, görünümün geçiş yapamadığı anlamına gelmez. DOM başarıyla güncellenmiş olabilir ancak yinelenen view-transition-name'ler olduğu için geçiş atlandı.

Bunun yerine:

Yapılması gerekenler
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  animateFromMiddle(transition);

  await transition.updateCallbackDone;
}

async function animateFromMiddle(transition) {
  try {
    await transition.ready;

    document.documentElement.animate(
      {
        clipPath: [`inset(50%)`, `inset(0)`],
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: '::view-transition-new(root)',
      }
    );
  } catch (err) {
    // You might want to log this error, but it shouldn't break the app
  }
}

Bu örnekte, DOM güncellemesini beklemek ve başarısız olursa reddetmek için transition.updateCallbackDone kullanılmaktadır. Geçiş başarısız olursa switchView artık reddetmez. DOM güncellemesi tamamlandığında çözümlenir ve başarısız olursa reddeder.

Yeni görünüm 'yerleştirildiğinde' (örneğin, animasyonlu geçiş tamamlandığında veya sonuna kadar atlandığında) switchView ürününün çözmesini istiyorsanız transition.updateCallbackDone öğesini transition.finished ile değiştirin.


Bir polyfill değil ancak…

Bu, çoklu doldurma için kolay bir özellik değildir. Ancak bu yardımcı işlev, görünüm geçişlerini desteklemeyen tarayıcılarda işleri çok daha kolay hale getirir:

function transitionHelper({
  skipTransition = false,
  types = [],
  update,
}) {

  const unsupported = (error) => {
    const updateCallbackDone = Promise.resolve(update()).then(() => {});

    return {
      ready: Promise.reject(Error(error)),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
      types,
    };
  }

  if (skipTransition || !document.startViewTransition) {
    return unsupported('View Transitions are not supported in this browser');
  }

  try {
    const transition = document.startViewTransition({
      update,
      types,
    });

    return transition;
  } catch (e) {
    return unsupported('View Transitions with types are not supported in this browser');
  }
}

Bu işlev şu şekilde kullanılabilir:

function spaNavigate(data) {
  const types = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    update() {
      updateTheDOMSomehow(data);
    },
    types,
  });

  // …
}

Görüntüleme geçişlerini desteklemeyen tarayıcılarda updateDOM çağrılmaya devam eder ancak animasyonlu geçiş olmaz.

Ayrıca, geçiş sırasında <html> öğesine eklenecek classNames sağlayabilirsiniz. Böylece, gezinme türüne bağlı olarak geçişi değiştirmeyi daha kolay hale getirebilirsiniz.

Görüntüleme geçişlerini destekleyen tarayıcılarda bile animasyon istemiyorsanız true öğesini skipTransition öğesine de iletebilirsiniz. Sitenizde geçişleri devre dışı bırakmaya yönelik bir kullanıcı tercihi varsa bu yararlı olur.


Çerçevelerle çalışma

DOM değişikliklerini soyutlayan bir kitaplık veya çerçeveyle çalışıyorsanız en zor kısım, DOM değişikliğinin ne zaman tamamlandığını bilmektir. Aşağıda, yukarıdaki yardımcıyı kullanarak çeşitli çerçevelerde bir dizi örnek verilmiştir.

  • Tepki: Buradaki anahtar, bir dizi durum değişikliğini eşzamanlı olarak uygulayan flushSync'tır. Evet, bu API'nin kullanımıyla ilgili büyük bir uyarı var, ancak Dan Abramov, bu durumda uygun olduğunu söylüyor. React ve eşzamansız kodda her zamanki gibi, startViewTransition tarafından döndürülen çeşitli özellikleri kullanırken kodunuzun doğru durumda çalıştığından emin olun.
  • Vue.js: Buradaki anahtar nextTick olup DOM güncellendikten sonra yerine getirilir.
  • Svelte: Vue'ya çok benzer ancak bir sonraki değişikliği bekleme yöntemi tick.
  • Açık: Buradaki temel nokta, bileşenlerde yer alan ve DOM güncellendikten sonra gerçekleşen this.updateComplete'tır.
  • Angular: Buradaki anahtar, bekleyen DOM değişikliklerini temizleyen applicationRef.tick'tır. Angular 17 sürümü itibarıyla @angular/router ile birlikte gelen withViewTransitions'ü kullanabilirsiniz.

API referansı

const viewTransition = document.startViewTransition(update)

Yeni bir ViewTransition başlatın.

update, dokümanın mevcut durumu yakalandığında çağrılan bir işlevdir.

Ardından, updateCallback tarafından verilen söz yerine getirildiğinde geçiş bir sonraki karede başlar. updateCallback tarafından verilen taahhüt reddedilirse geçiş işlemi yarıda bırakılır.

const viewTransition = document.startViewTransition({ update, types })

Belirtilen türlerle yeni bir ViewTransition başlat

Dokümanın geçerli durumu yakalandığında update çağrılır.

types, geçişi kaydederken veya gerçekleştirirken geçiş için etkin türleri ayarlar. Başlangıçta boştur. Daha fazla bilgi için aşağıya göz atın viewTransition.types.

ViewTransition örneğinin üyeleri:

viewTransition.updateCallbackDone

updateCallback tarafından döndürülen söz yerine getirildiğinde yerine getiren veya söz reddedildiğinde reddeden bir söz.

View Transition API, bir DOM değişikliğini sarmalayarak geçiş oluşturur. Bununla birlikte, bazen geçiş animasyonunun başarılı veya başarısız olması sizin için önemli değildir. Sadece DOM değişikliğinin olup olmadığını ve ne zaman gerçekleşeceğini bilmek istersiniz. updateCallbackDone, bu kullanım alanı içindir.

viewTransition.ready

Geçiş için sözde öğeler oluşturulduktan ve animasyon başlamak üzereyken yerine getirilen bir söz.

Geçiş başlayamazsa işlemi reddeder. Bu durum, yinelenen view-transition-name öğeleri gibi yanlış yapılandırmadan veya updateCallback ürününün reddedilen bir vaadi döndürmesinden kaynaklanıyor olabilir.

Bu, geçiş sözde öğelerini JavaScript ile canlandırmak için kullanışlıdır.

viewTransition.finished

Son durum kullanıcı tarafından tamamen görülebildiğinde ve etkileşime açık olduğunda yerine getirilen bir söz.

Yalnızca updateCallback, son durumun oluşturulmadığını belirttiği için reddedilmiş bir taahhüt döndürürse reddedilir.

Aksi takdirde, bir geçiş başlatılamazsa veya geçiş sırasında atlanırsa son duruma yine ulaşılır. Bu nedenle finished koşulu yerine getirilir.

viewTransition.types

Aktif görüntüleme geçişinin türlerini barındıran Set benzeri bir nesne. Girişleri değiştirmek için clear(), add() ve delete() örnek yöntemlerini kullanın.

CSS'deki belirli bir türe yanıt vermek için geçiş kökünde :active-view-transition-type(type) sözde sınıf seçiciyi kullanın.

Görünüm geçişi tamamlandığında türler otomatik olarak temizlenir.

viewTransition.skipTransition()

Geçişin animasyon kısmını atlayın.

DOM değişikliği geçişten ayrı olduğundan bu durumda updateCallback çağrısı atlanmaz.


Varsayılan stil ve geçiş referansı

::view-transition
Görüntü alanını dolduran ve her ::view-transition-group öğesini içeren kök sözde öğe.
::view-transition-group

Mutlaka konumlandırıldı.

"Öncesi" ile width ve height geçişleri ve "sonrası" eyaletler.

"Öncesi" arasındaki transform geçişler ve "sonrası" görüntü alanı-boşluk dörtlüsü.

::view-transition-image-pair

Mutlaka grubu dolduracak konumda.

mix-blend-mode öğesinin eski ve yeni görünümler üzerindeki etkisini sınırlandırmak için isolation: isolate kullanabilir.

::view-transition-new ve ::view-transition-old

Mutlaka sarmalayıcının sol üst tarafına yerleştirilmiş.

Grup genişliğinin %100'ünü doldurur ancak otomatik yüksekliğe sahiptir. Bu nedenle grubu doldurmak yerine en boy oranını korur.

Gerçek bir geçişe izin vermek için mix-blend-mode: plus-lighter değerine sahiptir.

Eski görünüm opacity: 1'ten opacity: 0'e geçer. Yeni görünüm opacity: 0 yerine opacity: 1.


Geri bildirim

Geliştirici geri bildirimleri bizim için çok değerli. Bunu yapmak için GitHub'da CSS Çalışma Grubu ile ilgili bir sorun bildirin. Bu bildirimde önerilerinizi ve sorularınızı paylaşın. Sorununuza [css-view-transitions] öneki ekleyin.

Hatayla karşılaşırsanız bunun yerine Chromium hata bildiriminde bulunun.