@starting-style ve allow-discrete ile animasyon
Bir elementi display: none durumundan görünür hale getirirken opacity ya da transform geçişi uygulamak istediğinizde CSS uzun süre boyunca sessiz kaldı. display bir discrete property olduğu için tarayıcı onu interpolate edemiyordu; element ya görünürdü ya değildi, arada bir şey yoktu. Çözüm hep JavaScript'ti: önce display: block yap, bir frame bekle (requestAnimationFrame ya da setTimeout), sonra class ekle. Bu yazı, artık buna gerek bırakmayan iki CSS özelliğini anlatıyor.
Sorun neydi?
CSS transition'ları iki değer arasında interpolasyon yaparak çalışır. opacity: 0 ile opacity: 1 arasında tarayıcı ara değerler üretebilir. Ama display: none ile display: block arasında ara değer yoktur; element ya render tree'de vardır ya yoktur.
Bunun pratik sonucu şuydu: bir dialog ya da dropdown'ı display: none ile gizleyip açarken fade-in yapamıyordunuz. visibility: hidden + opacity: 0 kombinasyonu kısmen işe yarıyordu ama element hâlâ layout'ta yer kaplıyordu, erişilebilirlik ağacında görünüyordu ve tıklanabilirdi. Popover API gibi yeni platform API'leri de arka planda display: none kullandığı için bu kısıtlama daha da belirgin hale geldi.
transition-behavior: allow-discrete
transition-behavior property'si tarayıcıya "bu discrete property'yi de transition'a dahil et" diyor. Değeri allow-discrete olduğunda tarayıcı, display gibi normalde geçiş yapamayan property'leri de transition süresince değiştiriyor.
Örneğin element açılırken display: none'dan display: block'a geçişte tarayıcı, transition süresinin başında display: block'u anında uygular ve diğer property'lerin (opacity, transform) geçişi boyunca element görünür kalır. Kapanırken ise tam tersi: transition bitene kadar display: block korunur, süre dolduğunda display: none'a düşer.
.panel {
display: none;
opacity: 0;
transition: opacity 300ms ease,
display 300ms ease allow-discrete;
}
.panel.open {
display: block;
opacity: 1;
}Burada display 300ms ease allow-discrete ifadesindeki allow-discrete, transition-behavior değerinin shorthand içinde yazılmış hali. Ayrı yazmak da mümkün ama shorthand daha kısa.
Bu tek başına açılış animasyonunu çözmüyor. Tarayıcı display: none'dan display: block'a geçtiğinde element render tree'ye ilk kez giriyor ve tarayıcı ilk frame'de hangi değerlerden başlayacağını bilmiyor. İşte burada @starting-style devreye giriyor.
@starting-style nedir?
@starting-style bir at-rule. Tarayıcıya "bu element render tree'ye ilk girdiğinde şu property değerlerinden başla" diyor. İlk frame'deki başlangıç değerlerini tanımlıyor, böylece transition'ın nereden nereye gideceği belli oluyor.
.panel {
display: none;
opacity: 0;
transform: translateY(8px);
transition: opacity 300ms ease,
transform 300ms ease,
display 300ms ease allow-discrete;
}
.panel.open {
display: block;
opacity: 1;
transform: translateY(0);
}
@starting-style {
.panel.open {
opacity: 0;
transform: translateY(8px);
}
}Akış şöyle işliyor:
.openclass'ı eklendiğinde tarayıcıdisplay: blockuygular, element render tree'ye girer.@starting-stylebloğundaki değerler ilk frame olarak kullanılır:opacity: 0,translateY(8px).- Transition hedef değerlere doğru çalışır:
opacity: 1,translateY(0).
Kapanışta .open class'ı kaldırılınca opacity ve transform tekrar eski değerlerine döner. allow-discrete sayesinde display: none bu transition bitene kadar bekler.
Gerçek bir dialog örneği
HTML <dialog> elementi ve Popover API, arka planda display: none ile gizleme yapar. Bu elementlere de aynı teknik uygulanabilir. Bir <dialog> için tam çalışan örnek:
<dialog id="dlg">
<p>Merhaba, ben bir dialog'um.</p>
<button onclick="this.closest('dialog').close()">Kapat</button>
</dialog>
<button onclick="document.getElementById('dlg').showModal()">Aç</button>dialog {
opacity: 0;
transform: scale(0.95);
transition: opacity 200ms ease,
transform 200ms ease,
display 200ms ease allow-discrete,
overlay 200ms ease allow-discrete;
}
dialog[open] {
opacity: 1;
transform: scale(1);
}
@starting-style {
dialog[open] {
opacity: 0;
transform: scale(0.95);
}
}
dialog::backdrop {
background: rgb(0 0 0 / 0);
transition: background 200ms ease,
display 200ms ease allow-discrete,
overlay 200ms ease allow-discrete;
}
dialog[open]::backdrop {
background: rgb(0 0 0 / 0.3);
}
@starting-style {
dialog[open]::backdrop {
background: rgb(0 0 0 / 0);
}
}Burada overlay property'si de allow-discrete ile transition'a dahil ediliyor. overlay top-layer'da kalma süresini kontrol ediyor; bu olmadan ::backdrop transition bitmeden kaybolabiliyor.
overlay property'si neden gerekli?
<dialog> ve popover elementleri top-layer'da render ediliyor. Element kapandığında top-layer'dan hemen çıkarılırsa backdrop ve element bir anda kaybolur, exit animasyonu görünmez. overlay property'sine allow-discrete eklendiğinde tarayıcı, transition süresi boyunca elementi top-layer'da tutuyor.
Bu sadece top-layer elementleri (dialog, popover) için geçerli. Normal bir div'i gizleyip gösterirken overlay'e ihtiyaç yok.
Popover API ile kullanım
Popover API ile de aynı yaklaşım birebir çalışıyor:
<button popovertarget="menu">Menü</button>
<div id="menu" popover>
<p>Popover içeriği</p>
</div>[popover] {
opacity: 0;
transform: translateY(-4px);
transition: opacity 150ms ease,
transform 150ms ease,
display 150ms ease allow-discrete,
overlay 150ms ease allow-discrete;
}
[popover]:popover-open {
opacity: 1;
transform: translateY(0);
}
@starting-style {
[popover]:popover-open {
opacity: 0;
transform: translateY(-4px);
}
}CSS anchor positioning ile birleştirildiğinde tooltip ve dropdown gibi bileşenler için tamamen kütüphanesiz bir çözüm ortaya çıkıyor.
@keyframes yerine neden transition?
@keyframes ile de display: none'dan çıkış animasyonu yapılabilir. Ama transition yaklaşımının belirgin bir avantajı var: exit animasyonu otomatik olarak çalışıyor. @keyframes ile exit animasyonu için ayrı bir animation tanımlayıp, elementin kapanma anını yakalayıp, animasyon bitince display: none uygulamanız gerekiyor. transition-behavior: allow-discrete bunu tek satırda hallediyor.
Bir dezavantaj: transition ile sadece iki durum arası geçiş yapılabiliyor. Çok adımlı bir animasyon gerekiyorsa (bounce efekti gibi) @keyframes hâlâ doğru seçim.
Tarayıcı desteği
Chrome 117 (Eylül 2023), Firefox 129 (Temmuz 2024) ve Safari 17.4 (Mart 2024) bu özellikleri destekliyor. Baseline 2024 kapsamında üç büyük tarayıcı motorunda da kullanılabilir durumda. Progressive enhancement stratejisi basit: destek yoksa element geçiş animasyonu olmadan açılıp kapanır, işlevsellik bozulmaz.
Sık yapılan hatalar
@starting-style bloğunu yanlış yere yazmak en yaygın hata. Bu blok, elementin hedef durumundaki selector'ı içermeli. Yani .panel değil, .panel.open selector'ı @starting-style içinde olmalı.
İkinci hata, display property'sini transition listesine eklemeyi unutmak. Sadece @starting-style yazmak yeterli değil; display de allow-discrete ile transition'a dahil edilmeli.
Üçüncü hata, exit animasyonunun çalışmaması. Bunun sebebi genellikle elementin kapalı durumdaki (display: none olan) stillerinde opacity ve transform değerlerinin tanımlı olmaması. Tarayıcının geri dönüş yapacağı değerler açık olmalı.
/* Yanlış: kapalı durumda hedef değer yok */
.panel {
display: none;
transition: opacity 300ms ease,
display 300ms ease allow-discrete;
}
/* Doğru: kapalı durumda opacity tanımlı */
.panel {
display: none;
opacity: 0;
transition: opacity 300ms ease,
display 300ms ease allow-discrete;
}Inline @starting-style
@starting-style ayrı bir blok olarak yazmak yerine doğrudan kural setinin içine de yazılabiliyor:
.panel.open {
display: block;
opacity: 1;
transform: translateY(0);
@starting-style {
opacity: 0;
transform: translateY(8px);
}
}Bu CSS nesting söz dizimi ile daha okunaklı. Hangi selector'a ait olduğu belirgin, dosyada ayrı bir yere bakmak gerekmiyor. Nesting desteği de Baseline 2023 kapsamında tüm tarayıcılarda var.
Ne zaman kullanmalı?
Bu iki özellik, display: none ile gizlenen her elementin entry/exit animasyonu için doğru araç. Dialog, popover, tooltip, dropdown, sidebar, notification toast. JavaScript'te requestAnimationFrame hack'i ya da animationend event listener'ı ile display toggle etme günleri bitti. Eğer animasyonun iki durumlu bir geçişten fazlası olması gerekiyorsa, yani ara adımlar, bounce, stagger gibi efektler lazımsa @keyframes kullanmak hâlâ daha doğru. Ama vakaların büyük çoğunluğu basit fade ve slide geçişleri, ve bunlar için artık üç satır CSS yeterli.