מעברים של תצוגה באותו מסמך באפליקציות של דף יחיד

מעבר בין תצוגה מפורטת פועל במסמך יחיד נקרא מעבר לתצוגה באותו מסמך. לרוב זה המצב באפליקציות עם דף יחיד (SPA), שבהן נעשה שימוש ב-JavaScript כדי לעדכן את ה-DOM. החל מגרסה 111 של Chrome, יש תמיכה במעברים בין תצוגות באותו מסמך.

כדי להפעיל מעבר לתצוגה באותו מסמך, צריך לבצע קריאה למספר document.startViewTransition:

function handleClick(e) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow();
    return;
  }

  // With a View Transition:
  document.startViewTransition(() => updateTheDOMSomehow());
}

כשמפעילים אותו, הדפדפן מצלם באופן אוטומטי תמונות מצב של כל הרכיבים שיש להם נכס CSS מסוג view-transition-name.

לאחר מכן היא מפעילה את הקריאה החוזרת (callback) שהועברה כדי לעדכן את ה-DOM, ולאחר מכן היא מבצעת snapshot של קובצי snapshot של המצב החדש.

תמונות המצב האלה מסודרות לאחר מכן בעץ של פסאודו-רכיבים ומונפשות באמצעות העוצמה של אנימציות CSS. זוגות של תמונות מצב מהמצב הישן והחדש עוברים בצורה חלקה מהמיקום והגודל הישנים שלהם למיקום החדש שלהם, והתוכן שלהם הצטמצם באופן הדרגתי. אם רוצים, אפשר להשתמש ב-CSS כדי להתאים אישית את האנימציות.


מעבר ברירת המחדל: עמעום הדרגתי

מעבר ברירת המחדל של התצוגה הוא עמעום הדרגתי, ולכן הוא משמש כמבוא נחמד ל-API:

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // With a transition:
  document.startViewTransition(() => updateTheDOMSomehow(data));
}

כאשר updateTheDOMSomehow משנה את ה-DOM למצב החדש. אפשר לעשות את זה איך שרוצים. לדוגמה, אפשר להוסיף או להסיר רכיבים, לשנות שמות של כיתות או לשנות סגנונות.

ובדיוק כך, הדפים הופכים לעמעום:

ברירת המחדל של עמעום הדרגתי. הדגמה מינימלית. מקור.

אוקיי, עמעום הדרגתי לא מרשים כל כך. למרבה המזל, אפשר להתאים אישית מעברים, אבל קודם כול צריך להבין איך הייתה הפעולה הבסיסית של עמעום הדרגתי.


איך המעברים האלה פועלים

נעדכן את דוגמת הקוד הקודמת.

document.startViewTransition(() => updateTheDOMSomehow(data));

כשמתבצעת קריאה אל .startViewTransition(), ה-API מתעד את המצב הנוכחי של הדף. זה כולל צילום תמונת מצב.

כשהתהליך יסתיים, תתבצע קריאה חוזרת (callback) אל .startViewTransition(). כאן ה-DOM משתנה. לאחר מכן, ה-API מתעד את המצב החדש של הדף.

אחרי שהמצב החדש מתועד, ה-API בונה עץ פסאודו-רכיב כמו בדוגמה הבאה:

::view-transition
└─ ::view-transition-group(root)
   └─ ::view-transition-image-pair(root)
      ├─ ::view-transition-old(root)
      └─ ::view-transition-new(root)

הקובץ ::view-transition נמצא בשכבת-על מעל לכל שאר הפריטים בדף. האפשרות הזו שימושית אם רוצים להגדיר צבע רקע למעבר.

::view-transition-old(root) הוא צילום מסך של התצוגה הישנה, ו-::view-transition-new(root) הוא ייצוג פעיל של התצוגה החדשה. שתיהן מעובדות כ 'תוכן שהוחלף' ב-CSS (כמו <img>).

בתצוגה הישנה יש אנימציה מ-opacity: 1 עד opacity: 0, ואילו התצוגה החדשה כוללת אנימציה מ-opacity: 0 עד opacity: 1, וכך נוצרת עמעום הדרגתי.

כל האנימציה מתבצעת באמצעות אנימציות CSS, כך שניתן להתאים אותן אישית באמצעות CSS.

התאמה אישית של המעבר

ניתן לטרגט באמצעות CSS את כל האלמנטים של מעבר תצוגה, ומכיוון שהאנימציות מוגדרות באמצעות CSS, תוכלו לשנות אותן באמצעות מאפייני אנימציה קיימים של CSS. למשל:

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 5s;
}

בעקבות השינוי הזה, אפקט הדעיכה איטי מאוד:

עמעום ארוך וממושך. הדגמה מינימלית. מקור.

אוקיי, זה עדיין לא מרשים. במקום זאת, הקוד הבא מיישם את מעבר הציר המשותף של Material Design:

@keyframes fade-in {
  from { opacity: 0; }
}

@keyframes fade-out {
  to { opacity: 0; }
}

@keyframes slide-from-right {
  from { transform: translateX(30px); }
}

@keyframes slide-to-left {
  to { transform: translateX(-30px); }
}

::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

והנה התוצאה:

מעבר בציר משותף. הדגמה מינימלית. מקור.

העברה של מספר רכיבים

בהדגמה הקודמת, כל הדף מעורב במעבר בין הציר המשותף. זה פועל לרוב הדף, אבל לא נראה שזה מתאים לכותרת, כי הוא יורד כדי להחליק בחזרה פנימה.

כדי להימנע מכך, ניתן לחלץ את הכותרת משאר הדף כדי שניתן יהיה להוסיף לה אנימציה בנפרד. כדי לעשות זאת, מקצים view-transition-name לרכיב.

.main-header {
  view-transition-name: main-header;
}

הערך של view-transition-name יכול להיות איך שרוצים (מלבד none, כלומר אין שם מעבר). הוא משמש לזיהוי ייחודי של הרכיב לאורך המעבר.

וכתוצאה מכך:

מעבר בציר משותף עם כותרת קבועה. הדגמה מינימלית. מקור.

עכשיו הכותרת נשארת במקום ומתעמעמת.

הצהרת ה-CSS הזו גרמה לשינוי בעץ הרכיב המדומה:

::view-transition
├─ ::view-transition-group(root)
│  └─ ::view-transition-image-pair(root)
│     ├─ ::view-transition-old(root)
│     └─ ::view-transition-new(root)
└─ ::view-transition-group(main-header)
   └─ ::view-transition-image-pair(main-header)
      ├─ ::view-transition-old(main-header)
      └─ ::view-transition-new(main-header)

יש עכשיו שתי קבוצות מעבר. אחת לכותרת ועוד אחת לכל השאר. אפשר לטרגט אליהם באופן עצמאי באמצעות שירות CSS, ולקבל מעברים שונים. למרות זאת, במקרה הזה main-header נשאר עם מעבר ברירת המחדל, שהוא עמעום הדרגתי.

טוב, מעבר ברירת המחדל הוא לא רק עמעום הדרגתי, ::view-transition-group גם עובר:

  • מיקום וטרנספורמציה (באמצעות transform)
  • רוחב
  • גובה

זה לא היה חשוב עד עכשיו, מכיוון שהכותרת היא באותו גודל ובמיקום של שני צידי שינוי ה-DOM. אבל אפשר גם לחלץ את הטקסט מהכותרת:

.main-header-text {
  view-transition-name: main-header-text;
  width: fit-content;
}

נעשה שימוש ב-fit-content כך שהרכיב יהיה גודל הטקסט במקום להימתח לרוחב הנותר. בלי זה, חץ החזרה מקטין את גודל רכיב טקסט הכותרת, במקום את אותו גודל בשני הדפים.

עכשיו יש לנו שלושה חלקים לשחק איתם:

::view-transition
├─ ::view-transition-group(root)
│  └─ …
├─ ::view-transition-group(main-header)
│  └─ …
└─ ::view-transition-group(main-header-text)
   └─ …

אבל שוב, נעבור עם ברירות המחדל:

הזזת טקסט הכותרת. הדגמה מינימלית. מקור.

עכשיו טקסט הכותרת מזיז את הדף באופן מעט רציף, כדי לפנות מקום ללחצן 'הקודם'.


אנימציה של מספר פסאודו אלמנטים באותו אופן עם view-transition-class

תמיכה בדפדפן

  • 125
  • 125
  • x
  • x

נניח שיש מעבר בין תצוגה עם כמה כרטיסים, אבל גם כותרת בדף. כדי להוסיף אנימציה לכל הכרטיסים מלבד שם הפריט, צריך לכתוב בורר שמטרגט כל כרטיס בנפרד.

h1 {
    view-transition-name: title;
}
::view-transition-group(title) {
    animation-timing-function: ease-in-out;
}

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
…
#card20 { view-transition-name: card20; }

::view-transition-group(card1),
::view-transition-group(card2),
::view-transition-group(card3),
::view-transition-group(card4),
…
::view-transition-group(card20) {
    animation-timing-function: var(--bounce);
}

יש לך 20 רכיבים? אלה 20 סלקטורים שאתם צריכים לכתוב. הוספת רכיב חדש? לאחר מכן צריך גם להגדיל את הבורר שמחיל את סגנונות האנימציה. לא ניתן להתאמה בדיוק.

אפשר להשתמש ב-view-transition-class בפסאודו-אלמנטים של מעבר התצוגה כדי להחיל את אותו כלל סגנון.

#card1 { view-transition-name: card1; }
#card2 { view-transition-name: card2; }
#card3 { view-transition-name: card3; }
#card4 { view-transition-name: card4; }
#card5 { view-transition-name: card5; }
…
#card20 { view-transition-name: card20; }

#cards-wrapper > div {
  view-transition-class: card;
}
html::view-transition-group(.card) {
  animation-timing-function: var(--bounce);
}

בדוגמה לכרטיסים הבאים נעשה שימוש בקטע הקוד הקודם של שירות ה-CSS. המערכת תחיל את אותו תזמון בכל הכרטיסים – כולל כרטיסים חדשים שהוספתם – באמצעות בורר אחד: html::view-transition-group(.card).

הקלטה של ההדגמה של הכרטיסים. כשמשתמשים בפונקציה view-transition-class, המערכת מחילה את אותו animation-timing-function על כל הכרטיסים, חוץ מאלה שנוספו או הוסרו.

מעברים של ניפוי באגים

מעברי תצוגה מבוססים על אנימציות CSS, ולכן החלונית אנימציות בכלי הפיתוח ל-Chrome היא כלי נהדר לניפוי באגים במעברים.

בחלונית אנימציות אפשר להשהות את האנימציה הבאה, ולאחר מכן לגלול קדימה ואחורה באנימציה. במהלך פרק הזמן הזה, תוכלו למצוא את רכיבי המעברון בחלונית רכיבים.

ניפוי באגים במעברים בין תצוגות מפורטות באמצעות כלי הפיתוח ל-Chrome.

רכיבי ההעברה לא חייבים להיות אותו רכיב DOM

עד עכשיו השתמשנו ב-view-transition-name כדי ליצור רכיבי מעבר נפרדים לכותרת ולטקסט בכותרת. זה בעיקרון אותו רכיב לפני ואחרי שינוי ה-DOM, אבל אפשר ליצור מעברים במקומות שבהם זה לא המצב.

לדוגמה, אפשר לתת view-transition-name להטמעת הסרטון הראשי:

.full-embed {
  view-transition-name: full-embed;
}

לאחר מכן, כשמשתמש לוחץ על התמונה הממוזערת, אפשר לתת לה את אותו view-transition-name, רק למשך המעבר:

thumbnail.onclick = async () => {
  thumbnail.style.viewTransitionName = 'full-embed';

  document.startViewTransition(() => {
    thumbnail.style.viewTransitionName = '';
    updateTheDOMSomehow();
  });
};

והתוצאה:

מעבר של רכיב אחד לרכיב אחר. הדגמה מינימלית. מקור.

עכשיו התמונה הממוזערת תעבור לתמונה הראשית. למרות שיש להם רכיבים שונים במהותם (ולמעשה), ממשק ה-API של המעבר מתייחס אליהם כרכיבים זהים, כי יש להם את אותו view-transition-name.

הקוד האמיתי של המעבר הזה קצת יותר מורכב מהדוגמה הקודמת, כי הוא מטפל גם במעבר חזרה לדף התמונה הממוזערת. כדי לקבל מידע על ההטמעה המלאה, אפשר לעיין במקור.


מעברים בהתאמה אישית לכניסה וליציאה

נבחן את הדוגמה הזו:

כניסה לסרגל הצד ויציאה ממנו. הדגמה מינימלית. מקור.

סרגל הצד הוא חלק מהמעבר:

.sidebar {
  view-transition-name: sidebar;
}

עם זאת, בניגוד לכותרת בדוגמה הקודמת, סרגל הצד לא מופיע בכל הדפים. אם בשתי המדינות יש סרגל צד, פסאודו-רכיבי המעבר נראים כך:

::view-transition
├─ …other transition groups…
└─ ::view-transition-group(sidebar)
   └─ ::view-transition-image-pair(sidebar)
      ├─ ::view-transition-old(sidebar)
      └─ ::view-transition-new(sidebar)

עם זאת, אם סרגל הצד נמצא רק בדף החדש, הרכיב ::view-transition-old(sidebar) לא יופיע. מאחר שאין תמונה 'ישנה' בסרגל הצד, בזוג התמונות יהיה רק ::view-transition-new(sidebar). באופן דומה, אם סרגל הצד נמצא רק בדף הישן, בצמד התמונות יהיה רק ::view-transition-old(sidebar).

בהדגמה הקודמת, סרגל הצד יכול לעבור באופן שונה בהתאם למכשיר הכניסה שלו, היציאה ממנו או הצגתו בשני המצבים. הכניסה אליו מתבצעת על ידי החלקה מימין והדרגתיות פנימה, היא יוצאת ממנו על ידי החלקה ימינה ו עמעום, ונשארת במקומה כאשר היא מופיעה בשני המצבים.

כדי ליצור מעברים ספציפיים לכניסה וליציאה, אפשר להשתמש בפסאודו-מחלקה :only-child כדי לטרגט את הרכיב הישן או החדש לרכיבים החדשים, אם הוא הילד היחיד בצמד התמונות:

/* Entry transition */
::view-transition-new(sidebar):only-child {
  animation: 300ms cubic-bezier(0, 0, 0.2, 1) both fade-in,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Exit transition */
::view-transition-old(sidebar):only-child {
  animation: 150ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-right;
}

במקרה הזה, אין מעבר ספציפי שבו סרגל הצד מוצג בשני המצבים, כי ברירת המחדל היא מושלמת.

עדכוני DOM אסינכרוניים ובהמתנה לתוכן

הקריאה החוזרת (callback) שמועברת אל .startViewTransition() יכולה להחזיר הבטחה, וכך לבצע עדכוני DOM אסינכרוניים ולהמתין שהתוכן חשוב יהיה מוכן.

document.startViewTransition(async () => {
  await something;
  await updateTheDOMSomehow();
  await somethingElse;
});

המעבר לא יתחיל עד שההבטחה תמומש. במהלך התקופה הזו הדף הוקפא, כך שצריך לצמצם ככל האפשר את העיכובים. באופן ספציפי, יש לבצע אחזורים של הרשת לפני הקריאה ל-.startViewTransition(), בזמן שהדף עדיין אינטראקטיבי באופן מלא, ולא לבצע אותן כחלק מהקריאה החוזרת של .startViewTransition().

אם תחליטו להמתין עד שהתמונות או הגופנים יהיו מוכנים, הקפידו להגדיר זמן קצוב לתפוגה באופן אגרסיבי:

const wait = ms => new Promise(r => setTimeout(r, ms));

document.startViewTransition(async () => {
  updateTheDOMSomehow();

  // Pause for up to 100ms for fonts to be ready:
  await Promise.race([document.fonts.ready, wait(100)]);
});

עם זאת, במקרים מסוימים עדיף להימנע לגמרי מהעיכוב, ולהשתמש בתוכן שכבר נמצא ברשותכם.


מפיקים את המרב מהתוכן שכבר יש לכם

במקרה שבו התמונה הממוזערת עוברת לתמונה גדולה יותר:

התמונה הממוזערת עוברת לתמונה גדולה יותר. אפשר לנסות את אתר ההדגמה.

מעבר ברירת המחדל הוא עמעום הדרגתי. המשמעות היא שהתמונה עשויה להופיע הדרגתית עם תמונה מלאה שעדיין לא נטענה.

דרך אחת לטפל בכך היא להמתין עד שהתמונה המלאה תיטען לפני שמתחילים במעבר. מומלץ לעשות זאת לפני הקריאה ל-.startViewTransition(), כך שהדף יישאר אינטראקטיבי וניתן להציג סימן גרפי שמצביע על כך למשתמש שהפעולות נטענים. אבל במקרה הזה יש דרך טובה יותר:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
}

עכשיו התמונה הממוזערת לא נעלמת, היא פשוט מוצבת מתחת לתמונה המלאה. המשמעות היא שאם התצוגה החדשה לא נטענה, התמונה הממוזערת תהיה גלויה לכל אורך המעבר. המשמעות היא שהמעבר יכול להתחיל באופן מיידי והתמונה המלאה יכולה להיטען בזמן משלה.

השיטה הזו לא הייתה מועילה אם בתצוגה החדשה מוצגת שקיפות, אבל במקרה הזה אנחנו יודעים שאין לה שקיפות, ולכן אנחנו יכולים לבצע את האופטימיזציה הזו.

איך מטפלים בשינויים ביחס גובה-רוחב

לנוחותך, כל המעברים עד כה היו לאלמנטים עם אותו יחס גובה-רוחב, אבל לא תמיד זה יהיה המצב. מה קורה אם התמונה הממוזערת היא ביחס 1:1 והתמונה הראשית היא 16:9?

מעבר של רכיב אחד לרכיב אחר, עם שינוי ביחס גובה-רוחב. הדגמה מינימלית. מקור.

במעבר ברירת המחדל, הקבוצה מכילה אנימציה מהגודל שלפני הגודל ומהגודל שאחרי הגודל שלו. התצוגה הישנה והתצוגה החדשה הן 100% מרוחב הקבוצה והגובה האוטומטי שלהן, כלומר הן שומרות על יחס הגובה-רוחב שלהן ללא קשר לגודל הקבוצה.

זוהי ברירת מחדל טובה, אך זו לא מה שרצית במקרה זה. אלה השלבים לביצוע:

::view-transition-old(full-embed),
::view-transition-new(full-embed) {
  /* Prevent the default animation,
  so both views remain opacity:1 throughout the transition */
  animation: none;
  /* Use normal blending,
  so the new view sits on top and obscures the old view */
  mix-blend-mode: normal;
  /* Make the height the same as the group,
  meaning the view size might not match its aspect-ratio. */
  height: 100%;
  /* Clip any overflow of the view */
  overflow: clip;
}

/* The old view is the thumbnail */
::view-transition-old(full-embed) {
  /* Maintain the aspect ratio of the view,
  by shrinking it to fit within the bounds of the element */
  object-fit: contain;
}

/* The new view is the full image */
::view-transition-new(full-embed) {
  /* Maintain the aspect ratio of the view,
  by growing it to cover the bounds of the element */
  object-fit: cover;
}

פירוש הדבר הוא שהתמונה הממוזערת נשארת במרכז האלמנט ככל שהרוחב מתרחב, אבל התמונה המלאה לא חתוכה בזמן שהיא עוברת מיחסים של 1:1 ל-16:9.

למידע מפורט יותר, אפשר לעיין במאמר הצגת מעברים: טיפול בשינויים ביחס גובה-רוחב


שימוש בשאילתות מדיה כדי לשנות מעברים במצבים שונים של מכשירים

ייתכן שתרצו להשתמש במעברים שונים בנייד לעומת במחשב שולחני, כמו בדוגמה הבאה, שבעזרתה תוכלו להציג שקף מלא מהצד בנייד, אבל להעביר שקף עדין יותר במחשב:

מעבר של רכיב אחד לרכיב אחר. הדגמה מינימלית. מקור.

אפשר לעשות זאת באמצעות שאילתות מדיה רגילות:

/* Transitions for mobile */
::view-transition-old(root) {
  animation: 300ms ease-out both full-slide-to-left;
}

::view-transition-new(root) {
  animation: 300ms ease-out both full-slide-from-right;
}

@media (min-width: 500px) {
  /* Overrides for larger displays.
  This is the shared axis transition from earlier in the article. */
  ::view-transition-old(root) {
    animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
  }

  ::view-transition-new(root) {
    animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in,
      300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
  }
}

כדאי גם לשנות את הרכיבים שמקצים view-transition-name בהתאם לשאילתות מדיה תואמות.


תגובה להעדפה של 'תנועה מופחתת'

המשתמשים יכולים לציין שהם מעדיפים תנועה מופחתת דרך מערכת ההפעלה שלהם, ושההעדפה הזו חשופה ב-CSS.

בחרת למנוע מעבר של המשתמשים האלה:

@media (prefers-reduced-motion) {
  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}

אבל ההעדפה של 'תנועה מופחתת' לא אומרת שהמשתמש רוצה ללא תנועה. במקום קטע הקוד הקודם, אפשר לבחור אנימציה עדינה יותר, שעדיין מבטאת את הקשר בין הרכיבים לזרימת הנתונים.


טיפול בכמה סגנונות של מעבר בין תצוגות באמצעות סוגי מעברים של תצוגות מפורטות

לפעמים מעבר מתצוגה מסוימת לתצוגה אחרת צריך לכלול מעבר מותאם אישית. לדוגמה, כשעוברים לדף הקודם או לדף הקודם ברצף העימוד, ייתכן שתרצו להסיט את התוכן בכיוון שונה, בהתאם לדף שמוביל לדף גבוה יותר או לדף נמוך יותר מהרצף.

הקלטה של הדגמת העימוד. היא משתמשת במעברים שונים בהתאם לדף שאליו נכנסים.

לשם כך, תוכלו להשתמש בסוגי מעבר בין תצוגות מפורטות, שמאפשרים לכם להקצות סוג אחד או יותר למעבר בין תצוגה פעילה. לדוגמה, כשעוברים לדף גבוה יותר ברצף החלוקה לדפים, צריך להשתמש בסוג forwards וכשעוברים לדף נמוך יותר, צריך להשתמש בסוג backwards. הסוגים האלה פעילים רק כשמצלמים או מבצעים מעבר, ואפשר להתאים אישית כל סוג באמצעות CSS כדי להשתמש באנימציות שונות.

כדי להשתמש בסוגים במסגרת מעבר לתצוגה באותו מסמך, צריך להעביר את types לשיטה startViewTransition. כדי לאפשר זאת, document.startViewTransition מקבל גם אובייקט: update היא פונקציית הקריאה החוזרת שמעדכנת את ה-DOM ו-types היא מערך עם הסוגים.

const direction = determineBackwardsOrForwards();

const t = document.startViewTransition({
  update: updateTheDOMSomehow,
  types: ['slide', direction],
});

כדי להגיב לסוגים האלה, צריך להשתמש בבורר :active-view-transition-type(). מעבירים את type שרוצים לטרגט אל הבורר. כך אפשר להפריד בין הסגנונות של כמה מעברים בין תצוגות, בלי שההצהרות של האחד יפריעו להצהרות של התצוגה השנייה.

מאחר שסוגים חלים רק במהלך תיעוד או ביצוע של המעבר, תוכלו להשתמש בבורר כדי להגדיר view-transition-name ברכיב או לבטל את ההגדרה שלו רק לצורך המעבר לתצוגה מהסוג הזה.

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only (using the default root snapshot) */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

בהדגמה של החלוקה לדפים, תוכן הדף מחליקים קדימה או אחורה בהתאם למספר הדף שאליו אתם מנווטים. הסוגים נקבעים לפי הקליק שאליו הם מועברים אל document.startViewTransition.

כדי לטרגט מעבר של תצוגה פעילה, ללא קשר לסוג, אפשר להשתמש במקום זאת בבורר המחלקה המדומה :active-view-transition.

html:active-view-transition {
    …
}

טיפול בסגנונות מעבר של תצוגה מפורטת עם שם מחלקה ברמה הבסיסית של המעבר לתצוגה המפורטת

לפעמים מעבר מסוג מסוים לתצוגה אחרת צריך לכלול מעבר מותאם אישית. לחלופין, הניווט 'הקודם' צריך להיות שונה מניווט ל 'הבא'.

מעברים שונים כשחוזרים אל 'חזרה'. הדגמה מינימלית. מקור.

לפני סוגי המעבר, הטיפול במקרים האלה היה להגדיר באופן זמני שם מחלקה ברמה הבסיסית (root) של המעבר. כשמבצעים קריאה ל-document.startViewTransition, שורש המעבר הזה הוא הרכיב <html>, שאליו ניתן לגשת באמצעות document.documentElement ב-JavaScript:

if (isBackNavigation) {
  document.documentElement.classList.add('back-transition');
}

const transition = document.startViewTransition(() =>
  updateTheDOMSomehow(data)
);

try {
  await transition.finished;
} finally {
  document.documentElement.classList.remove('back-transition');
}

כדי להסיר את הכיתות אחרי שהמעבר יסתיים, בדוגמה הזו נעשה שימוש ב-transition.finished, הבטחה שמתבטלת כשהמעבר מגיע למצב הסיום שלו. מאפיינים אחרים של האובייקט הזה מפורטים בהפניה ל-API.

עכשיו אפשר להשתמש בשם המחלקה הזה ב-CSS כדי לשנות את המעבר:

/* 'Forward' transitions */
::view-transition-old(root) {
  animation: 90ms cubic-bezier(0.4, 0, 1, 1) both fade-out,
    300ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left;
}

::view-transition-new(root) {
  animation: 210ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 300ms
      cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right;
}

/* Overrides for 'back' transitions */
.back-transition::view-transition-old(root) {
  animation-name: fade-out, slide-to-right;
}

.back-transition::view-transition-new(root) {
  animation-name: fade-in, slide-from-left;
}

בדומה לשאילתות מדיה, אפשר להשתמש בנוכחות של המחלקות האלה גם כדי לשנות את הרכיבים שמקבלים view-transition-name.


הפעלת מעברים בלי להקפיא אנימציות אחרות

כדאי לצפות בהדגמה הזו של מיקום מעבר של סרטון:

מעבר לסרטון. הדגמה מינימלית. מקור.

האם ראית משהו לא בסדר? אל דאגה, גם אם לא. כאן הוא מואט:

מעבר בין סרטונים, איטי יותר. הדגמה מינימלית. מקור.

במהלך המעבר, נראה שהסרטון קופא, ולאחר מכן גרסת ההפעלה של הסרטון נעלמת. הסיבה לכך היא ש-::view-transition-old(video) הוא צילום מסך של התצוגה הישנה, ואילו ::view-transition-new(video) הוא תמונה פעילה של התצוגה החדשה.

אפשר לתקן את זה, אבל קודם כול, כדאי לשאול את עצמך אם כדאי לתקן. אם לא ראית את ה'בעיה' כשהמעבר פעלו במהירות הרגילה, לא אשמח לשנות אותה.

אם אתם באמת רוצים לתקן את זה, אל תציגו את ::view-transition-old(video). יש לעבור ישירות אל ::view-transition-new(video). כדי לעשות את זה אפשר לעקוף את הסגנונות והאנימציות שמוגדרים כברירת מחדל:

::view-transition-old(video) {
  /* Don't show the frozen old view */
  display: none;
}

::view-transition-new(video) {
  /* Don't fade the new view in */
  animation: none;
}

זהו, סיימתם.

מעבר בין סרטונים, איטי יותר. הדגמה מינימלית. מקור.

עכשיו הסרטון מופעל לאורך המעבר.


אנימציה עם JavaScript

עד עכשיו כל המעברים הוגדרו באמצעות CSS, אבל לפעמים שירות CSS לא מספיק:

מעבר מעגלי. הדגמה מינימלית. מקור.

יש כמה חלקים מהמעבר הזה שאי אפשר להשיג באמצעות שירות CSS בלבד:

  • האנימציה מתחילה מהמיקום של הקליק.
  • האנימציה מסתיימת בעיגול עם רדיוס מהפינה המרוחקת ביותר. אבל אני מקווה שהדבר יהיה אפשרי באמצעות שירות CSS בעתיד.

למרבה המזל, אפשר ליצור מעברים באמצעות Web Animation API.

let lastClick;
addEventListener('click', event => (lastClick = event));

function spaNavigate(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    updateTheDOMSomehow(data);
    return;
  }

  // Get the click position, or fallback to the middle of the screen
  const x = lastClick?.clientX ?? innerWidth / 2;
  const y = lastClick?.clientY ?? innerHeight / 2;
  // Get the distance to the furthest corner
  const endRadius = Math.hypot(
    Math.max(x, innerWidth - x),
    Math.max(y, innerHeight - y)
  );

  // With a transition:
  const transition = document.startViewTransition(() => {
    updateTheDOMSomehow(data);
  });

  // Wait for the pseudo-elements to be created:
  transition.ready.then(() => {
    // Animate the root's new view
    document.documentElement.animate(
      {
        clipPath: [
          `circle(0 at ${x}px ${y}px)`,
          `circle(${endRadius}px at ${x}px ${y}px)`,
        ],
      },
      {
        duration: 500,
        easing: 'ease-in',
        // Specify which pseudo-element to animate
        pseudoElement: '::view-transition-new(root)',
      }
    );
  });
}

בדוגמה הזו נעשה שימוש ב-transition.ready, הבטחה שמתבטלת לאחר שרכיבי המעבר לכאורה נוצרים בהצלחה. מאפיינים אחרים של האובייקט הזה מפורטים בהפניה ל-API.


מעברים כשיפור

View Transit API נועד 'לעטוף' שינוי DOM וליצור מעבר עבורו. עם זאת, יש להתייחס למעבר כאל שיפור, כמו כן, אסור לאפליקציה שלך להזין מצב 'שגיאה' אם השינוי ב-DOM מצליח, אבל המעבר נכשל. באופן אידיאלי המעבר לא אמור להיכשל, אבל אם הוא ייכשל, הוא לא אמור לפגוע בשאר חוויית המשתמש.

כדי להתייחס למעברים כשיפור, הקפידו לא להשתמש בהבטחות מעבר באופן שיגרום לאפליקציה לפעול אם המעבר ייכשל.

מה אסור לעשות
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  await transition.ready;

  document.documentElement.animate(
    {
      clipPath: [`inset(50%)`, `inset(0)`],
    },
    {
      duration: 500,
      easing: 'ease-in',
      pseudoElement: '::view-transition-new(root)',
    }
  );
}

הבעיה בדוגמה הזו היא ש-switchView() ידחה אם המעבר לא יכול להגיע למצב ready, אבל זה לא אומר שהחלפת התצוגה נכשלה. יכול להיות שה-DOM עודכן בהצלחה, אבל היו עותקים כפולים של view-transition-name, ולכן המערכת דילגה על המעבר.

במקום זאת:

מה מותר לעשות
async function switchView(data) {
  // Fallback for browsers that don't support this API:
  if (!document.startViewTransition) {
    await updateTheDOM(data);
    return;
  }

  const transition = document.startViewTransition(async () => {
    await updateTheDOM(data);
  });

  animateFromMiddle(transition);

  await transition.updateCallbackDone;
}

async function animateFromMiddle(transition) {
  try {
    await transition.ready;

    document.documentElement.animate(
      {
        clipPath: [`inset(50%)`, `inset(0)`],
      },
      {
        duration: 500,
        easing: 'ease-in',
        pseudoElement: '::view-transition-new(root)',
      }
    );
  } catch (err) {
    // You might want to log this error, but it shouldn't break the app
  }
}

בדוגמה הזו נעשה שימוש ב-transition.updateCallbackDone כדי להמתין לעדכון ה-DOM וכדי לדחות אם הוא נכשל. switchView לא דוחה יותר אם המעבר נכשל, הוא מסתיים כשעדכון ה-DOM מסתיים ודוחה אם הוא נכשל.

אם רוצים שהפעולה switchView תפתור את הבעיה כשבוצעה הגדרה של התצוגה החדשה, כמו בכל מעבר עם אנימציה שהושלם או שהיא דילגה לסוף, צריך להחליף את הערך transition.updateCallbackDone בטקסט transition.finished.


לא מילוי פוליגון, אבל...

זו לא תכונה קלה ל-polyfill. עם זאת, בעזרת פונקציית העזרה הזו, קל יותר לבצע פעולות בדפדפנים שלא תומכים במעברים בין תצוגות:

function transitionHelper({
  skipTransition = false,
  types = [],
  update,
}) {

  const unsupported = (error) => {
    const updateCallbackDone = Promise.resolve(update()).then(() => {});

    return {
      ready: Promise.reject(Error(error)),
      updateCallbackDone,
      finished: updateCallbackDone,
      skipTransition: () => {},
      types,
    };
  }

  if (skipTransition || !document.startViewTransition) {
    return unsupported('View Transitions are not supported in this browser');
  }

  try {
    const transition = document.startViewTransition({
      update,
      types,
    });

    return transition;
  } catch (e) {
    return unsupported('View Transitions with types are not supported in this browser');
  }
}

ניתן להשתמש בו כך:

function spaNavigate(data) {
  const types = isBackNavigation ? ['back-transition'] : [];

  const transition = transitionHelper({
    update() {
      updateTheDOMSomehow(data);
    },
    types,
  });

  // …
}

בדפדפנים שלא תומכים במעברים בין תצוגות, updateDOM עדיין תופעל, אבל לא תהיה מעבר מונפש.

אפשר גם להוסיף כמה classNames ל<html> במהלך המעבר, וכך קל יותר לשנות את המעבר בהתאם לסוג הניווט.

אפשר גם להעביר את true אל skipTransition אם לא רוצים ליצור אנימציה, גם בדפדפנים שתומכים במעברים בין תצוגות. האפשרות הזו שימושית אם לאתר שלכם יש העדפת משתמש להשבית מעברים.


עבודה עם frameworks

אם אתם עובדים עם ספרייה או framework שמפשטים שינויי DOM, החלק המורכב הוא לדעת מתי שינוי ה-DOM הושלם. הנה סדרת דוגמאות, תוך שימוש בכלי העזר שלמעלה, במסגרות שונות.

  • תגובה – המפתח כאן הוא flushSync, שמחיל קבוצה של שינויי מצב באופן סינכרוני. כן, יש אזהרה גדולה לגבי השימוש ב-API הזה, אבל דן אברמוב מבטיח לי שהוא מתאים במקרה הזה. כמו בכל קוד תגובה וקוד אסינכרוני, כשמשתמשים בהבטחות השונות שהוחזרו על ידי startViewTransition, צריך לוודא שהקוד פועל במצב הנכון.
  • Vue.js – המפתח כאן הוא nextTick, שממלא אחרי עדכון ה-DOM.
  • Svelte – דומה מאוד ל-Vue, אבל השיטה להמתין לשינוי הבא היא tick.
  • Lit – המפתח כאן הוא ההבטחה this.updateComplete בתוך הרכיבים, שמתממשת אחרי שה-DOM מתעדכן.
  • Angular – המפתח כאן הוא applicationRef.tick, שמנקה שינויי DOM בהמתנה. החל מגרסה 17 של Angular, אפשר להשתמש ב-withViewTransitions שמגיע עם @angular/router.

הפניית API

const viewTransition = document.startViewTransition(update)

התחלת ViewTransition חדש.

update הוא פונקציה מופעלת לאחר תיעוד המצב הנוכחי של המסמך.

לאחר מכן, כשההבטחה שהוחזרה על ידי updateCallback תמומש, המעבר יתחיל בפריים הבא. אם ההבטחה שהוחזרה על ידי updateCallback תידחה, המעבר יופסק.

const viewTransition = document.startViewTransition({ update, types })

התחלת ViewTransition חדש עם הסוגים שצוינו

מתבצעת קריאה אל update אחרי תיעוד המצב הנוכחי של המסמך.

types מגדיר את הסוגים הפעילים למעבר בזמן תיעוד או ביצוע של המעבר. בהתחלה היא ריקה. מידע נוסף זמין ב-viewTransition.types למטה.

חברים במכונה של ViewTransition:

viewTransition.updateCallbackDone

הבטחה שמתממשת כשההבטחה שהוחזרה על ידי updateCallback ממומשת או נדחית כשהיא דוחה אותה.

View Transit API כולל שינוי DOM ויוצר מעבר. עם זאת, לפעמים לא מעניינים אתכם ההצלחה או הכישלון של אנימציית המעבר, אלא רוצים לדעת אם ומתי מתרחש שינוי ה-DOM. updateCallbackDone מיועד לתרחיש הזה.

viewTransition.ready

הבטחה שמתממשת לאחר יצירת פסאודו הרכיבים של המעבר, והאנימציה עומדת להתחיל.

אם ההעברה לא יכולה להתחיל, היא תידחה. המצב הזה יכול לנבוע מהגדרה שגויה, כמו כפילויות של view-transition-name, או אם updateCallback מחזיר הובטחה שנדחתה.

האפשרות הזו שימושית ליצירת אנימציה של פסאודו-אלמנטים של המעבר באמצעות JavaScript.

viewTransition.finished

הבטחה שמתממשת ברגע שמצב הקצה גלוי ואינטראקטיבי למשתמש.

היא תידחה רק אם updateCallback מחזירה הבטחה שנדחתה, כי היא מצביעה על כך שהמצב הסופי לא נוצר.

אחרת, אם מעבר לא מתחיל, או אם מדלגים עליו במהלך המעבר, מצב הסיום עדיין יגיע, כך ש-finished מתקיים.

viewTransition.types

אובייקט כמו Set שמכיל את סוגי המעבר של התצוגה הפעילה. כדי לשנות את הרשומות, משתמשים ב-methods של המכונה clear(), add() ו-delete().

כדי להשיב לסוג ספציפי ב-CSS, משתמשים בבורר המדומה :active-view-transition-type(type) ברמה הבסיסית (root) של המעבר.

סוגי הפריטים ינוקו באופן אוטומטי בסיום המעבר בין התצוגה.

viewTransition.skipTransition()

דילוג על החלק של האנימציה במעבר.

הפעולה הזו לא תדלג על קריאה אל updateCallback, כי שינוי ה-DOM נפרד מהמעבר.


מסמך עזר בנושא סגנון ברירת מחדל ומעברים

::view-transition
רכיב השורש שממלא את אזור התצוגה ומכיל כל ::view-transition-group.
::view-transition-group

במיקום מוחלט.

מעבר width ו-height בין המצבים 'לפני' ו 'אחרי'.

מעביר transform בין הריבועים של אזור התצוגה 'לפני' ו 'אחרי'.

::view-transition-image-pair

נמצא בעמדה מלאה למילוי הקבוצה.

מכיל isolation: isolate כדי להגביל את ההשפעה של mix-blend-mode על התצוגה המפורטת הישנה והחדשה.

::view-transition-new וגם ::view-transition-old

נמצא בדיוק בפינה השמאלית העליונה של ה-wrapper.

המודעה ממלאת 100% מרוחב הקבוצה, אבל כוללת גובה אוטומטי ולכן היא שומרת על יחס הגובה-רוחב שלה ולא תמלא את הקבוצה.

כולל mix-blend-mode: plus-lighter כדי לאפשר עמעום אמיתי.

התצוגה הישנה עוברת מ-opacity: 1 ל-opacity: 0. התצוגה החדשה עוברת מ-opacity: 0 ל-opacity: 1.


משוב

המשוב מהמפתחים תמיד חשוב לנו. כדי לעשות זאת, צריך לדווח על בעיה בקבוצת העבודה של CSS ב-GitHub עם הצעות ושאלות. מוסיפים את קידומת הבעיה [css-view-transitions] לתחילת הבעיה.

אם תיתקלו בבאג, דווחו על באג ב-Chromium במקום זאת.