في السابق، لم يكن بإمكان تأثيرات CSS الانتقالية تحريك العناصر عند إضافتها لأول مرة إلى DOM أو عند تغيير السمة display من none. توفّر القاعدة @@starting-style وtransition-behavior: allow-discrete طريقة تعريفية لإنشاء تأثيرات حركية سلسة للدخول والخروج.
التنفيذ
1. جارٍ تحريك 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;
}
}
2. إضافة تأثيرات حركية على إدراج 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();
القيود وتسهيل الاستخدام
- إلزامي: استخدِم
transition-behavior: allow-discreteعند نقلdisplay. بدون ذلك، سيختفي العنصر على الفور أثناء الخروج. - لا تستخدِم
allow-discreteفي اختصارtransition، لأنّ ذلك سيؤدي إلى تجاهل المتصفحات القديمة لبيانtransitionبالكامل. باستثناء حالات الاستخدام التي يكون فيها ذلك مرغوبًا، استخدِم بيانtransition-behavior: allow-discreteمنفصلاً. - إلزامي: استخدِم
@starting-styleلحركات الدخول. تتخطّى المتصفّحات الانتقالات في أول تعديل لنمط العنصر (العرض الأولي أو تغييرdisplay: none) ما لم يتم توفير ذلك. - ما يجب فعله: يجب تضمين
overlayفي قائمةtransitionفي حال إضافة حركة إلى عناصر الطبقة العليا، مثل<dialog>أوpopover، لضمان بقائها في الطبقة العليا أثناء الحركة عند الخروج. - ما يجب فعله: احترِم الإعدادات المفضّلة للمستخدمين في ما يتعلّق بتقليل الحركة باستخدام الاستعلام عن الوسائط
prefers-reduced-motion. - لا تعتمد على
@starting-styleفي حركات الخروج، لأنّها تحدّد فقط نقطة البداية لحركة الدخول. يتم تحديد حركات الخروج من خلال الانتقال إلى الحالة المخفية.
استراتيجيات احتياطية
الخيارات الاحتياطية وتوافق المتصفّح مع transition-behavior
يحلّل الإصدار 129 من Firefox والإصدارات الأحدث 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 على جميع المتصفّحات الحديثة بدون JavaScript. عندما تكون انتقالات display المنفصلة غير متاحة (!canTransitionDisplay())، يؤدي ضبط hidden إلى تطبيق display: none على الفور وتخطّي حركة الخروج (عند الإخفاء وقبل element.remove()). افصل حالة الخروج المرئية ([data-closing]) عن display: none ([hidden]) حتى ينتهي opacity وtranslate من الحركة قبل إخفاء العنصر أو إزالته. استبدِل قواعد .card[hidden] من الخطوة 1 (بما في ذلك إلغاء 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();
}