Öğe Giriş ve Çıkışına Animasyon Ekleme

Geçmişte CSS geçişleri, DOM'a ilk eklendiklerinde veya display özellikleri none olarak değiştirildiğinde öğelere animasyon uygulayamıyordu. @starting-style kuralı ve transition-behavior: allow-discrete, sorunsuz giriş ve çıkış animasyonları oluşturmak için bildirim temelli bir yöntem sunar.

Uygulama

1. display: none açma/kapatma düğmesi animasyonu

Bir öğeyi, görünürlüğünü bir özellik aracılığıyla açıp kapatırken canlandırmak için (ör. display: none ile hidden):

  1. Görünür durumu tanımlayın: Temel sınıfta nihai özellik değerlerini (ör. opacity: 1) ayarlayın.
  2. Giriş başlangıç durumunu tanımlama: Öğe görünür hale geldiğinde hangi değerlerden geçiş yapılacağını belirtmek için @starting-style kullanın.
  3. Ayrık geçişleri etkinleştirme: transition özelliğine display değerini ekleyin ve transition-behavior: allow-discrete değerini kullanın.
  4. Çıkış durumunu tanımlayın: hidden özelliğinde hedef değerleri ayarlayın.
.card {
  display: block;
  opacity: 1;
  translate: 0;
  /* MANDATORY: Use transition-behavior: allow-discrete for display transition */
  transition:
    display 0.4s,
    opacity 0.4s ease-out,
    translate 0.4s ease-out;
  transition-behavior: allow-discrete;
}

/* Entry animation: transition FROM these values when first rendered */
@starting-style {
  .card {
    opacity: 0;
    translate: 0 -20px;
  }
}

/* Exit animation: transition TO these values when hidden */
.card[hidden] {
  display: none;
  opacity: 0;
  translate: 0 -20px;
}

/* Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
  .card {
    /* Disable movement and shorten duration for a simple fade */
    translate: none;
    transition-duration: 0.1s;
  }

  @starting-style {
    .card {
      translate: none;
    }
  }

  .card[hidden] {
    translate: none;
  }
}

2. DOM Ekleme ve Kaldırma İşlemlerini Animasyonla Gösterme

appendChild() ile eklenen veya remove() ile kaldırılan öğeler için:

  • Giriş: Yukarıda gösterildiği gibi @starting-style kullanın. Tarayıcı, "hiçbir şey"den öğenin ilk stillerine geçişi otomatik olarak algılar ve @starting-style değerlerinden geçişi tetikler.
  • Kaldırma: element.remove() anında gerçekleştiğinden ve kendi başına bir CSS geçişini tetiklemediğinden, çıkış geçişini önce tetiklemeniz ve düğümü DOM'dan kaldırmadan önce bitmesini beklemeniz gerekir.
// 1. Trigger exit transition
element.hidden = true;

// 2. Wait for all active transitions/animations to finish,
//    with a failsafe timeout in case an animation never ends (e.g. for looping animations)
const animations = element.getAnimations();
if (animations.length > 0) {
  await Promise.race([
    // Promise.allSettled ensures we wait even if some animations fail
    Promise.allSettled(animations.map(a => a.finished)),
    new Promise(r => setTimeout(r, 2000))
  ]);
}

// 3. Finally remove the node from the DOM
element.remove();

Kısıtlamalar ve erişilebilirlik

  • ZORUNLU: display geçişi yaparken transition-behavior: allow-discrete kullanın. Bu özellik kullanılmazsa öğe, çıkış sırasında anında kaybolur.
  • transition kısaltmasında allow-discrete KULLANMAYIN. Bu, eski tarayıcıların tüm transition bildirimini yok saymasına neden olur. Bunun istendiği kullanım alanları dışında ayrı bir transition-behavior: allow-discrete beyanı kullanın.
  • ZORUNLU: Giriş animasyonları için @starting-style kullanın. Tarayıcılar, bu sağlanmadığı sürece bir öğenin ilk stil güncellemesindeki (ilk oluşturma veya display: none değişikliği) geçişleri atlar.
  • DO: <dialog> veya popover gibi üst katman öğelerine animasyon ekliyorsanız çıkış animasyonu sırasında üst katmanda kalmalarını sağlamak için overlay öğesini transition listesine ekleyin.
  • DO: prefers-reduced-motion medya sorgusunu kullanarak kullanıcıların daha az hareketle ilgili tercihlerine saygı gösterin.
  • YAPMAYIN: Çıkış animasyonları için @starting-style kullanmayın. Bu özellik yalnızca giriş geçişinin başlangıç noktasını tanımlar. Çıkış animasyonları, gizli duruma geçişle tanımlanır.

Yedek stratejiler

transition-behavior için yedekler ve tarayıcı desteği

Firefox 129 ve sonraki sürümler, display özelliğini gerçekten geçiş yapmadan transition-behavior: allow-discrete (CSS.supports('transition-behavior', 'allow-discrete'), true değerini döndürür) ayrıştırır (Firefox hatası 1882408). Bu durum, öğelerin çıkışta hemen kaybolmasına neden olur.

Ayrık display geçiş desteğini güvenilir bir şekilde algılamak için yalnızca CSS.supports('transition-behavior', 'allow-discrete') özelliğine güvenmek yerine, geçici bir öğenin hesaplanan display değerinin none değerine geçiş yaparken görünür kalıp kalmadığını kontrol edin:

let supportsDisplayTransition;
function canTransitionDisplay() {
  if (supportsDisplayTransition !== undefined) return supportsDisplayTransition;
  if (!window.CSS?.supports?.('transition-behavior', 'allow-discrete') || !document.body) {
    return false;
  }
  const probe = document.createElement('div');
  // The shorthand is intentional here: browsers that don't parse allow-discrete
  // drop the whole declaration, so display: none applies instantly and the probe
  // correctly returns false. !important guards against global reduced-motion
  // resets like `* { transition: none !important }`.
  probe.style.cssText = 'transition: display 1s allow-discrete !important; display: block;';
  document.body.appendChild(probe);
  getComputedStyle(probe).display;
  probe.style.display = 'none';
  supportsDisplayTransition = getComputedStyle(probe).display === 'block';
  probe.remove();
  return supportsDisplayTransition;
}

Ayrık display geçişleri desteklenmediğinde çıkış yedeklemesi

@starting-style kullanan giriş animasyonları, JavaScript olmadan tüm modern tarayıcılarda çalışır. Ayrı display geçişleri desteklenmediğinde (!canTransitionDisplay()) hidden ayarı display: none değerini hemen uygular ve çıkış animasyonunu atlar (hem gizlerken hem de element.remove() öncesinde). Görsel çıkış durumunu ([data-closing]) display: none ([hidden]) değerinden ayırın. Böylece opacity ve translate, öğeyi gizlemeden veya kaldırmadan önce animasyonu tamamlar. 1. adımdaki .card[hidden] kurallarını (prefers-reduced-motion geçersiz kılma dahil) şunlarla değiştirin:

.card:where([hidden], [data-closing]) {
  opacity: 0;
  translate: 0 -20px;
}

.card[hidden] {
  display: none;
}

@media (prefers-reduced-motion: reduce) {
  .card:where([hidden], [data-closing]) {
    translate: none;
  }
}
async function hideElement(el) {
  if (canTransitionDisplay()) {
    el.hidden = true;
  } else {
    el.setAttribute('data-closing', '');
  }

  const animations = el.getAnimations();
  if (animations.length > 0) {
    await Promise.race([
      Promise.allSettled(animations.map((a) => a.finished)),
      new Promise((r) => setTimeout(r, 2000)),
    ]);
  }

  // Skipped if showElement() cancelled the close mid-animation.
  if (el.hasAttribute('data-closing')) {
    el.removeAttribute('data-closing');
    el.hidden = true;
  }
}

function showElement(el) {
  el.removeAttribute('data-closing');
  el.hidden = false;
}

async function removeElement(el) {
  await hideElement(el);
  el.remove();
}