پویانمایی ورود و خروج عنصر

ویژگی‌های وب: @starting-style transition-behavior

در گذشته، انتقال‌های CSS نمی‌توانستند عناصر را هنگام اضافه شدن اولیه به DOM یا زمانی که ویژگی display آن‌ها از none تغییر می‌کرد، پویانمایی کنند. قانون @starting-style و transition-behavior: allow-discrete روشی اعلانی برای ایجاد پویانمایی‌های ورود و خروج روان ارائه می‌دهند.

پیاده‌سازی

۱. تغییر وضعیت display: none پویانمایی

برای پویانمایی کردن عنصر هنگام تغییر وضعیت رؤیت‌پذیری آن ازطریق یک مشخصه (مثلاً hidden با display: none):

  1. تعریف کردن حالت نمایان: مقادیر خصوصیت نهایی (مثلاً opacity: 1) را در کلاس پایه تنظیم کنید.
  2. تعریف حالت شروع ورودی: از @starting-style برای مشخص کردن مقادیری که هنگام نمایان شدن عنصر باید از آن‌ها گذار انجام شود استفاده کنید.
  3. فعال کردن گذارهای گسسته: display را در ویژگی transition اضافه کنید و از 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;
  }
}

۲. پویانمایی درج و حذف DOM

برای عناصری که ازطریق appendChild() اضافه شده‌اند یا ازطریق remove() برداشته شده‌اند:

  • ورودی: از @starting-style همان‌طور که در بالا نشان داده شده است استفاده کنید. مرورگر به‌طور خودکار تغییر سبک از «هیچ» به سبک‌های اولیه عنصر را شناسایی می‌کند و انتقال از مقادیر @starting-style را راه‌اندازی می‌کند.
  • برداشتن: ازآنجایی‌که element.remove() آنی است و به‌خودی‌خود انتقال CSS را راه‌اندازی نمی‌کند، باید ابتدا انتقال خروج را راه‌اندازی کنید و منتظر بمانید تا تمام شود و سپس گره را از 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();

محدودیت‌ها و دسترس‌پذیری

  • MANDATORY: Use transition-behavior: allow-discrete when transitioning display. بدون آن، عنصر فوراً درطول خروج ناپدید می‌شود.
  • از allow-discrete در خلاصه transition استفاده نکنید — این کار باعث می‌شود مرورگرهای قدیمی‌تر کل اعلان transition را نادیده بگیرند. به‌جز در موارد استفاده‌ای که این کار مطلوب است، از یک اظهارنامه transition-behavior: allow-discrete جداگانه استفاده کنید.
  • اجباری: از @starting-style برای پویانمایی‌های ورودی استفاده کنید. مرورگرها از انتقال‌ها در اولین به‌روزرسانی سبک عنصر (رندر اولیه یا تغییر display: none) صرف‌نظر می‌کنند، مگراینکه این مورد ارائه شده باشد.
  • باید: اگر عناصر لایه بالایی مثل <dialog> یا popover را پویانمایی می‌کنید، overlay را در فهرست transition قرار دهید تا مطمئن شوید که درطول پویانمایی خروج در لایه بالایی باقی می‌مانند.
  • باید: بااستفاده از پُرسمان رسانه‌ای prefers-reduced-motion، به اولویت‌های کاربر برای کاهش حرکت احترام بگذارید.
  • نباید: برای پویانمایی‌های خروج به @starting-style اتکا کنید؛ این ویژگی فقط نقطه شروع را برای گذر ورودی تعریف می‌کند. پویانمایی‌های خروج با گذار به حالت پنهان تعریف می‌شوند.

راهبردهای جایگزین

جایگزین‌ها و پشتیبانی مرورگر برای transition-behavior

‫Firefox 129+‏ transition-behavior: allow-discrete را تجزیه می‌کند (CSS.supports('transition-behavior', 'allow-discrete') مقدار true را برمی‌گرداند) بدون اینکه واقعاً ویژگی display را انتقال دهد (اشکال Firefox 1882408)، که باعث می‌شود عناصر بلافاصله پس‌از خروج ناپدید شوند.

برای تشخیص مطمئن پشتیبانی از انتقال 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 در همه مرورگرهای مدرن بدون جاوا اسکریپت کار می‌کنند. وقتی گذارهای گسسته 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();
}