كان من الصعب في السابق تحريك العناصر التي يتم عرضها في "الطبقة العليا" (مثل <dialog> أو العناصر التي تتضمّن السمة popover أو تلميحات الشاشة) لأنّها تنتقل بين display: none والحالة المرئية. توفّر CSS الحديثة السمتَين @starting-style وtransition-behavior: allow-discrete والسمة overlay لإتاحة تأثيرات سلسة للدخول والخروج لهذه العناصر. يُرجى العِلم أنّه يتم استخدام تداخل CSS الأصلي في الأمثلة أدناه.
التنفيذ
1. تفعيل الانتقالات المنفصلة
لتحريك السمة display، يجب ضبط transition-behavior: allow-discrete. يتيح ذلك للعنصر أن يظل مرئيًا أثناء انتقاله للخروج. في حال استخدام اختصارات الانتقال، تأكَّد من وضع transition-behavior: allow-discrete بعد ذلك لمنع الاختصار من إلغائه.
2. الموقع الإلكتروني "overlay"
عندما ينتقل عنصر إلى الطبقة العليا أو يخرج منها، يجب أن ينقل السمة overlay. يضمن ذلك بقاء العنصر في الطبقة العليا طوال مدة الحركة، ما يمنع اقتصاصه بواسطة عناصر أخرى أو إطار العرض قبل الأوان.
3- صور متحركة للدخول باستخدام "@starting-style"
استخدِم القاعدة @starting-style لتحديد الأنماط التي يجب أن ينتقل إليها العنصر من عند عرضه لأول مرة أو عندما تتغيّر السمة display من none.
4. جارٍ إنشاء الصورة الخلفية المتحركة
يمكن أيضًا تحريك العنصر الزائف ::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
يحلّل الإصدار 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;
}
الخروج الاحتياطي من الطبقة العليا
تعمل تأثيرات الحركة عند الدخول في 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 API في الغالب على التحسين التدريجي، ولكن سلوكياتها المحدّدة، مثل الترقية إلى الطبقة العليا والإغلاق البسيط واستدعاء popovertarget، ليس لها مكافئ في CSS فقط. تحتاج المتصفّحات القديمة إلى polyfill أو إلى إجراء احتياطي يدوي إذا كنت تفضّل عدم شحنها.
Polyfill: لإتاحة السمة popover في المتصفّحات القديمة، يمكنك تحميل @oddbird/popover-polyfill بشكل مشروط. إجراء إلزامي: يجب رصد الميزة من خلال التحقّق من السمة popover في HTMLElement.prototype، وتحميل polyfill فقط في حال عدم توفّر الدعم الأصلي، ويجب عدم تحميله بدون شروط.
باستخدام حزمة أو خريطة استيراد:
// 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>
تحذير بشأن التنسيق: لا يمكن لـ polyfill تحديد الفئة الزائفة :popover-open الحقيقية، لذا يتم تطبيق فئة .\:popover-open بدلاً من ذلك. إجراء إلزامي: ادمج السمتَين مع :is() أو :where()، وإلا ستتجاهل المتصفحات التي لا تتضمّن :popover-open القاعدة بأكملها:
[popover]:is(:popover-open, .\:popover-open) {
display: block;
}
بدلاً من ذلك، يمكنك استخدام position: fixed واحتساب الإحداثيات يدويًا من خلال getBoundingClientRect() أو الاعتماد على تحديد الموضع التلقائي باستخدام inset: auto إذا كان ذلك مقبولاً لحالة الاستخدام، وذلك كحل بديل قديم بدون polyfill.