בעבר, מעברי 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 (באג 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;
}
יציאה למצב חלופי כשאין תמיכה במעברים נפרדים של 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();
}