एलिमेंट की एंट्री और एग्ज़िट को ऐनिमेट करना

वेब सुविधाएं: @starting-style transition-behavior

पहले, सीएसएस ट्रांज़िशन की मदद से एलिमेंट को ऐनिमेट नहीं किया जा सकता था. ऐसा तब होता था, जब उन्हें पहली बार DOM में जोड़ा जाता था या उनकी display प्रॉपर्टी को none से बदला जाता था. @starting-style ऐट-रूल और transition-behavior: allow-discrete, एंट्री और एग्ज़िट के लिए स्मूद ऐनिमेशन बनाने का एक डिक्लेरेटिव तरीका उपलब्ध कराते हैं.

लागू करना

1. display: none टॉगल को ऐनिमेट करना

किसी एट्रिब्यूट (जैसे, display: none के साथ hidden) की मदद से, किसी एलिमेंट की विज़िबिलिटी को टॉगल करते समय उसे ऐनिमेट करने के लिए:

  1. दिखने वाली स्थिति तय करना: बेस क्लास पर फ़ाइनल प्रॉपर्टी वैल्यू (जैसे, opacity: 1) सेट करें.
  2. एंट्री की शुरुआती स्थिति तय करना: एलिमेंट दिखने पर, कहां से ट्रांज़िशन करना है, यह बताने के लिए @starting-style का इस्तेमाल करें.
  3. डिसक्रीट ट्रांज़िशन चालू करना: transition प्रॉपर्टी में display शामिल करें और transition-behavior: allow-discrete का इस्तेमाल करें.
  4. एग्ज़िट स्टेट तय करना: hidden एट्रिब्यूट में टारगेट वैल्यू सेट करें.
.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. डीओएम इंसर्शन और रिमूवल को ऐनिमेट करना

appendChild() की मदद से जोड़े गए या remove() की मदद से हटाए गए एलिमेंट के लिए:

  • एंट्री: ऊपर दिखाए गए तरीके से @starting-style का इस्तेमाल करें. ब्राउज़र, स्टाइल में हुए बदलाव का अपने-आप पता लगा लेगा. जैसे, "कुछ नहीं" से एलिमेंट के शुरुआती स्टाइल में बदलाव. साथ ही, @starting-style वैल्यू से ट्रांज़िशन को ट्रिगर करेगा.
  • हटाना: element.remove() तुरंत लागू होता है और अपने-आप सीएसएस ट्रांज़िशन को ट्रिगर नहीं करता. इसलिए, आपको पहले एग्ज़िट ट्रांज़िशन को ट्रिगर करना होगा और DOM से नोड को हटाने से पहले, इसके खत्म होने का इंतज़ार करना होगा.
// 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();

पाबंदियां और सुलभता

  • ज़रूरी: display को ट्रांज़िशन करते समय transition-behavior: allow-discrete का इस्तेमाल करें. इसके बिना, एलिमेंट एग्ज़िट के दौरान तुरंत गायब हो जाएगा.
  • transition शॉर्टहैंड में allow-discrete का इस्तेमाल न करें — इससे पुराने ब्राउज़र, पूरे transition डिक्लेरेशन को अनदेखा कर देंगे. अगर आपको किसी खास मामले में ऐसा करना है, तो अलग से transition-behavior: allow-discrete एलान का इस्तेमाल करें.
  • ज़रूरी: एंट्री ऐनिमेशन के लिए, @starting-style का इस्तेमाल करें. ब्राउज़र, किसी एलिमेंट के स्टाइल में किए गए पहले अपडेट (शुरुआती रेंडर या display: none में बदलाव) पर ट्रांज़िशन को स्किप कर देते हैं. हालांकि, ऐसा तब नहीं होता, जब यह जानकारी दी गई हो.
  • ये करें: अगर <dialog> या popover जैसे टॉप-लेयर एलिमेंट को ऐनिमेट किया जा रहा है, तो overlay को transition सूची में शामिल करें. इससे यह पक्का किया जा सकेगा कि वे एग्ज़िट ऐनिमेशन के दौरान टॉप लेयर में बने रहें.
  • क्या करें: prefers-reduced-motion मीडिया क्वेरी का इस्तेमाल करके, कम मोशन के लिए उपयोगकर्ता की प्राथमिकताओं का सम्मान करें.
  • ऐसा न करें: एग्ज़िट ऐनिमेशन के लिए, @starting-style पर भरोसा न करें. यह सिर्फ़ एंट्री ट्रांज़िशन के लिए शुरुआती पॉइंट तय करता है. एग्ज़िट ऐनिमेशन, छिपी हुई स्थिति में ट्रांज़िशन से तय होते हैं.

फ़ॉलबैक रणनीतियां

फ़ॉलबैक और transition-behavior के लिए ब्राउज़र की सुविधा

Firefox 129 और इसके बाद के वर्शन, display प्रॉपर्टी (Firefox की गड़बड़ी 1882408) को ट्रांज़िशन किए बिना ही transition-behavior: allow-discrete (CSS.supports('transition-behavior', 'allow-discrete'), true दिखाता है) को पार्स करते हैं. इस वजह से, एलिमेंट बाहर निकलने पर तुरंत गायब हो जाते हैं.

display ट्रांज़िशन के लिए अलग-अलग सपोर्ट का पता लगाने के लिए, यह जांच करें कि किसी अस्थायी एलिमेंट का कंप्यूट किया गया display, none पर ट्रांज़िशन करने पर दिखता है या नहीं. इसके लिए, सिर्फ़ CSS.supports('transition-behavior', 'allow-discrete') पर भरोसा न करें:

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;
}

डिसक्रीट display ट्रांज़िशन के काम न करने पर, फ़ॉलबैक से बाहर निकलें

@starting-style का इस्तेमाल करके बनाए गए एंट्री ऐनिमेशन, JavaScript के बिना भी सभी मॉडर्न ब्राउज़र पर काम करते हैं. जब डिसक्रीट display ट्रांज़िशन काम नहीं करते (!canTransitionDisplay()), तो hidden सेट करने पर display: none तुरंत लागू हो जाता है और एग्ज़िट ऐनिमेशन को स्किप कर देता है. ऐसा, छिपाने और element.remove() से पहले, दोनों मामलों में होता है. विज़ुअल एग्ज़िट स्टेट ([data-closing]) को display: none ([hidden]) से अलग करें, ताकि एलिमेंट को छिपाने या हटाने से पहले, opacity और translate ऐनिमेशन खत्म हो जाए. पहले चरण में बताए गए .card[hidden] नियमों (prefers-reduced-motion को बदलने वाले नियम के साथ) को इससे बदलें:

.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();
}