پویانمایی عناصر به و از لایه بالایی

عناصری که در «لایه بالایی» ارائه می‌شوند (مانند <dialog>، عناصر با ویژگی popover، یا نکات ابزار) از گذشته برای پویانمایی دشوار بوده‌اند زیرا بین display: none و حالت قابل‌مشاهده جابه‌جا می‌شوند. CSS مدرن @starting-style، transition-behavior: allow-discrete، و ویژگی overlay را برای فعال کردن انتقال‌های ورود و خروج روان برای این عناصر ارائه می‌دهد. توجه داشته باشید که در مثال‌های زیر از تودرتو کردن CSS بومی استفاده شده است.

پیاده‌سازی

۱. فعال کردن گذارهای گسسته

برای پویانمایی کردن خصوصیت display، باید transition-behavior: allow-discrete را تنظیم کنید. این کار باعث می‌شود عنصر درطول گذار خروج نمایان بماند. اگر از خلاصه انتقال استفاده می‌کنید، حتماً transition-behavior: allow-discrete را بعداز آن قرار دهید تا خلاصه آن را نفی نکند.

۲. دارایی overlay

وقتی عنصری به لایه بالایی وارد یا از آن خارج می‌شود، باید ویژگی overlay را انتقال دهد. این کار تضمین می‌کند که عنصر درطول مدت پویانمایی در لایه بالایی باقی بماند و از برش زودرس آن توسط عناصر دیگر یا نمای هدف جلوگیری می‌کند.

۳. پویانمایی‌های ورودی با @starting-style

از قانون @starting-style برای تعریف سبک‌هایی استفاده کنید که عنصر باید هنگام اولین رندر شدن یا تغییر display از none، از آن‌ها انتقال یابد.

۴. درحال پویانمایی کردن پساویز

شبه‌عنصر ::backdrop را می‌توان به‌طور مشابه با اعمال انتقال به ویژگی‌های خودش پویانمایی کرد.

مثال

/* 1. Define the visible (open) state */
dialog[open],
[popover]:popover-open {
  opacity: 1;
  transform: scale(1);

  /* 2. Define the starting state for entry (must come after open state) */
  @starting-style {
    opacity: 0;
    transform: scale(0.9);
  }
}

/* 3. Define the base (closed/exit) state and transitions */
dialog,
[popover] {
  opacity: 0;
  transform: scale(0.9);

  /* MANDATORY: transition display and overlay for top-layer elements */
  transition-property: opacity, transform, display, overlay;
  transition-duration: 0.3s;
  transition-timing-function: ease-out;
  /* Applies to discrete properties like display and overlay */
  transition-behavior: allow-discrete; /* Note: be sure to write this after the shorthand */
}

/* 4. Animate the backdrop */
dialog::backdrop,
[popover]::backdrop {
  background-color: rgba(0, 0, 0, 0);
  transition-property: background-color, display, overlay;
  transition-duration: 0.3s;
  transition-timing-function: ease-out;
  transition-behavior: allow-discrete;
}

dialog[open]::backdrop,
[popover]:popover-open::backdrop {
  background-color: rgba(0, 0, 0, 0.5);

  @starting-style {
    background-color: rgba(0, 0, 0, 0);
  }
}

/* 5. Respect user preference for reduced motion */
@media (prefers-reduced-motion: reduce) {
  dialog,
  [popover] {
    /* Disable movement and shorten duration for a simple fade */
    transform: none;
    transition-duration: 0.1s;
  }

  @starting-style {
    dialog[open],
    [popover]:popover-open {
      transform: none;
    }
  }
}

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

  • اجباری: برای هر عنصری که به لایه بالایی منتقل می‌شود یا از آن خارج می‌شود، overlay را در فهرست transition خود اضافه کنید.
  • اجباری: از allow-discrete برای انتقال ویژگی display استفاده کنید.
  • اجباری: با ساده کردن انتقال‌ها (مثلاً برداشتن تبدیل‌ها و کوتاه کردن مدت)، به اولویت‌های کاربر برای کاهش حرکت بااستفاده از prefers-reduced-motion احترام بگذارید.
  • باید: بلوک @starting-style را داخل یا بعداز انتخابگر حالت «باز» قرار دهید تا آبشار به‌درستی انجام شود.
  • نباید: از @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;
}

بازگشت خروج لایه بالایی

پویانمایی‌های ورودی در CSS خالص در همه مرورگرهایی که از @starting-style پشتیبانی می‌کنند کار می‌کنند—هیچ کلاس .is-opening لازم نیست زیرا انتقال‌های ورودی به انتقال‌های overlay یا display گسسته وابسته نیستند.

پویانمایی‌های خروج به پشتیبانی از گذار overlay و display گسسته نیاز دارند. وقتی هیچ‌کدام پشتیبانی نمی‌شود (مثلاً در Firefox و Safari)، انتخابگرهای حالت باز را در :is() بپیچید و :where(:not([data-closing])) (آشیانه‌سازی &::backdrop و @starting-style) را اضافه کنید تا تنظیم data-closing انتقال خروج را راه‌اندازی کند درحالی‌که عنصر در لایه بالایی باقی می‌ماند، سپس منتظر بمانید تا getAnimations() قبل‌از فراخوانی .close() یا .hidePopover() مستقر شود:

:is(dialog[open], [popover]:popover-open):where(:not([data-closing])) {
  opacity: 1;
  transform: scale(1);

  @starting-style {
    opacity: 0;
    transform: scale(0.9);
  }

  &::backdrop {
    background-color: rgb(0 0 0 / 0.5);

    @starting-style {
      background-color: transparent;
    }
  }
}
// Evaluate lazily: canTransitionDisplay() needs document.body, so a top-level
// const would be permanently false if this script runs in <head>.
function supportsTopLayerExit() {
  return window.CSS?.supports?.('overlay', 'auto') && canTransitionDisplay();
}

async function closeTopLayer(element) {
  if (!supportsTopLayerExit()) {
    element.setAttribute('data-closing', '');
    const animations = element.getAnimations({ subtree: true });
    if (animations.length > 0) {
      await Promise.race([
        Promise.allSettled(animations.map((a) => a.finished)),
        new Promise((r) => setTimeout(r, 2000)),
      ]);
    }
    if (!element.hasAttribute('data-closing')) return;
    element.removeAttribute('data-closing');
  }

  element.close();
  // Or for popover:
  // element.hidePopover();
}

// Route native close requests (Esc, closedby light dismiss) through the same
// helper so they animate too, instead of closing instantly.
dialog.addEventListener('cancel', (event) => {
  event.preventDefault();
  closeTopLayer(dialog);
});

نمی‌توان رد کردن پاپ‌اور و کلیدهای popovertarget را رهگیری کرد (beforetoggle فقط هنگام باز شدن قابل‌لغو است)، بنابراین در مرورگرهایی که به بازگشت نیاز دارند، این خروج‌ها فوری هستند. اگر پویانمایی خروج مهم است، کنترل بستن صریحی ارائه دهید که closeTopLayer() را فراخوانی کند.

پشتیبانی مرورگر و بازگشت به حالت قبلی برای Popover

«میانای برنامه‌سازی کاربردی Popover» عمدتاً ارتقای پیش‌رو است، اما رفتارهای تعریف‌کننده آن (ارتقای لایه بالایی، رد کردن با کلیک در خارج از عنصر، و فراخوانی popovertarget) معادل CSS-only ندارند. مرورگرهای قدیمی‌تر به شبیه‌ساز مرورگر نیاز دارند، یا اگر ترجیح می‌دهید شبیه‌ساز مرورگر ارسال نکنید، به بازگشت دستی نیاز دارند.

شبیه‌ساز مرورگر: برای پشتیبانی از مشخصه popover در مرورگرهای قدیمی‌تر، @oddbird/popover-polyfill را به‌صورت شرطی بار کنید. اجباری: ویژگی را با بررسی کردن ویژگی popover در HTMLElement.prototype شناسایی کنید و شبیه‌ساز مرورگر را فقط زمانی بار کنید که پشتیبانی بومی وجود ندارد — آن را بدون قیدوشرط بار نکنید.

با بسته‌بندی یا نقشه واردات:

// MANDATORY: Feature detect 'popover' on HTMLElement.prototype.
if (!("popover" in HTMLElement.prototype)) {
  import("@oddbird/popover-polyfill");
}

بدون بسته‌بندی، از CDN داخل <script type="module"> وارد کنید:

<script type="module">
  if (!("popover" in HTMLElement.prototype)) {
    import("https://unpkg.com/@oddbird/popover-polyfill@latest/dist/popover.min.js");
  }
</script>

هشدار سبک‌بندی: شبیه‌ساز مرورگر نمی‌تواند کلاس کاذب واقعی :popover-open را تعریف کند، بنابراین به‌جای آن کلاس .\:popover-open را اعمال می‌کند. اجباری: این دو را با :is() یا :where() ترکیب کنید، درغیراین‌صورت مرورگرهایی که فاقد :popover-open هستند کل قانون را نادیده می‌گیرند:

[popover]:is(:popover-open, .\:popover-open) {
  display: block;
}

یا برای بازگشت به نسخه قدیمی بدون شبیه‌ساز مرورگر، از position: fixed استفاده کنید و مختصات را به‌صورت دستی ازطریق getBoundingClientRect() محاسبه کنید یا اگر برای مورد استفاده قابل‌قبول است، به موقعیت‌یابی پیش‌فرض با inset: auto تکیه کنید.