בעבר היה קשה להוסיף אנימציה לרכיבים שמופיעים ב "שכבה העליונה" (כמו <dialog>, רכיבים עם המאפיין popover או הסברים קצרים), כי הם עוברים בין display: none למצב גלוי. ב-CSS מודרני יש את @starting-style, transition-behavior: allow-discrete ואת המאפיין overlay כדי לאפשר מעברי כניסה ויציאה חלקים לאלמנטים האלה. שימו לב שבדוגמאות הבאות נעשה שימוש ב-CSS nesting מקורי.
הטמעה
1. הפעלת מעברים נפרדים
כדי להוסיף אנימציה למאפיין display, צריך להגדיר את transition-behavior: allow-discrete. כך הרכיב נשאר גלוי במהלך מעבר היציאה שלו. אם משתמשים בקיצורי דרך למעברים, צריך להציב את transition-behavior: allow-discrete אחריהם כדי למנוע מקיצור הדרך לבטל אותם.
2. הנכס overlay
כשאלמנט נכנס לשכבה העליונה או יוצא ממנה, הוא צריך להעביר את המאפיין overlay. כך הרכיב נשאר בשכבה העליונה למשך האנימציה, ולא נחתך על ידי רכיבים אחרים או אזור התצוגה לפני הזמן.
3. אנימציות כניסה עם @starting-style
משתמשים בכלל at-rule @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 (באג 1882408 ב-Firefox), מה שגורם לרכיבים להיעלם מיד ביציאה.
כדי לזהות באופן מהימן תמיכה במעבר 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 הוא בעיקר שיפור הדרגתי (progressive enhancement), אבל להתנהגויות שמגדירות אותו – קידום לשכבה העליונה, סגירה בלחיצה מחוץ לאלמנט והפעלה של popovertarget – אין מקבילה ב-CSS בלבד. בדפדפנים ישנים יותר צריך להשתמש ב-polyfill, או להגדיר חלופה ידנית אם אתם מעדיפים לא להשתמש ב-polyfill.
Polyfill: כדי לתמוך במאפיין popover בדפדפנים ישנים יותר, צריך לטעון את @oddbird/popover-polyfill באופן מותנה. חובה: צריך לזהות את התכונה על ידי בדיקת המאפיין popover ב-HTMLElement.prototype, ולטעון את ה-polyfill רק כשאין תמיכה מקומית – אל תטענו אותו ללא תנאי.
עם bundler או מפת ייבוא:
// 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;
}
לחלופין, כדי להשתמש ב-fallback מדור קודם בלי polyfill, אפשר להשתמש ב-position: fixed ולחשב קואורדינטות באופן ידני באמצעות getBoundingClientRect() או להסתמך על מיקום ברירת המחדל עם inset: auto אם זה מתאים לתרחיש לדוגמה.