در گذشته، انتقالهای CSS نمیتوانستند عناصر را هنگام اضافه شدن اولیه به DOM یا زمانی که ویژگی display آنها از none تغییر میکرد، پویانمایی کنند. قانون @starting-style و transition-behavior: allow-discrete روشی اعلانی برای ایجاد پویانماییهای ورود و خروج روان ارائه میدهند.
پیادهسازی
۱. تغییر وضعیت display: none پویانمایی
برای پویانمایی کردن عنصر هنگام تغییر وضعیت رؤیتپذیری آن ازطریق یک مشخصه (مثلاً hidden با display: none):
- تعریف کردن حالت نمایان: مقادیر خصوصیت نهایی (مثلاً
opacity: 1) را در کلاس پایه تنظیم کنید. - تعریف حالت شروع ورودی: از
@starting-styleبرای مشخص کردن مقادیری که هنگام نمایان شدن عنصر باید از آنها گذار انجام شود استفاده کنید. - فعال کردن گذارهای گسسته:
displayرا در ویژگیtransitionاضافه کنید و ازtransition-behavior: allow-discreteاستفاده کنید. - تعریف حالت خروج: مقادیر هدف را در مشخصه
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-discretewhen transitioningdisplay. بدون آن، عنصر فوراً درطول خروج ناپدید میشود. - از
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();
}