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

כשהמעבר בין התצוגות פועל במסמך יחיד הוא נקרא מעבר לתצוגת אותו מסמך. זה המצב בדרך כלל באפליקציות עם דף יחיד (SPA) שבהן נעשה שימוש ב-JavaScript לעדכון ה-DOM. מעברים בין תצוגות של מסמכים זהים נתמכים ב-Chrome החל מגרסה 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.

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

לאחר מכן, תמונות המצב האלה מסודרות בעץ של פסאודו-רכיבים ואנימציה באמצעות העוצמה של אנימציות 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 מתעד את המצב הנוכחי של הדף. האיסור הזה כולל צילום תמונת מצב.

בסיום התהליך, הקריאה החוזרת שמועברת אל .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 נשאר עם מעבר ברירת המחדל, שהוא מעבר הדרגתי (cross-fade).

בסדר, מעבר ברירת המחדל הוא לא רק מעבר הדרגתי, אלא גם ::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 אסינכרוניים, והמתנה לתוכן

הקריאה החוזרת שמועברת אל .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.

מידע מפורט יותר זמין בקטע 'הצגת המעברים: טיפול בשינויים ביחס גובה-רוחב' (https://jakearchibald.com/2024/view-transitions-handling-aspect-ratio-changes/)


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

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

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

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

/* 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 ל-method 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) של מעבר התצוגה המפורטת

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

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

כדי לטפל במקרים כאלה, לפני סוגי מעבר אפשר להגדיר באופן זמני שם מחלקה ברמה הבסיסית (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.


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

ממשק ה-API למעבר של View נועד 'לעטוף' שינוי 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, אבל...

זו לא תכונה קלה למילוי 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 אם לא רוצים לראות אנימציה, גם בדפדפנים שתומכים במעברי תצוגה. האפשרות הזו שימושית אם באתר שלכם יש העדפת משתמש להשבית מעברים.


עבודה עם מסגרות

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

  • תגובה – המפתח כאן הוא flushSync, שמחיל קבוצה של שינויי מצב באופן סינכרוני. כן, יש אזהרה גדולה לגבי השימוש ב-API הזה, אבל דן אברמוב מבטיח לי שזה מתאים במקרה הזה. כמו תמיד עם קוד React וקוד אסינכרוני, כשמשתמשים בהבטחות השונות שהוחזרו על ידי startViewTransition, חשוב לוודא שהקוד פועל במצב הנכון.
  • Vue.js – המפתח כאן הוא nextTick, שמולא לאחר עדכון ה-DOM.
  • Svelte – דומה מאוד ל-Vue, אבל השיטה בהמתנה לשינוי הבא היא tick.
  • Lit – המפתח כאן הוא ההבטחה this.updateComplete בתוך הרכיבים, שמתקיים אחרי עדכון ה-DOM.
  • אנגרי – המפתח כאן הוא 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 Migrate API אורז שינוי DOM ויוצר מעבר. עם זאת, לפעמים ההצלחה או הכישלון של אנימציית המעבר לא חשובה לכם, ואתם רוצים לדעת אם ומתי מתרחש שינוי ה-DOM. בתרחיש לדוגמה הזה, updateCallbackDone הוא תרחיש לדוגמה.

viewTransition.ready

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

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

האפשרות הזו שימושית אם אתם רוצים להנפיש את רכיבי המעבר של המעבר באמצעות JavaScript.

viewTransition.finished

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

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

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

viewTransition.types

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

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

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

viewTransition.skipTransition()

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

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


הסבר על סגנון ברירת המחדל ומעבר

::view-transition
הרכיב המדומה הבסיסי (root) שממלא את אזור התצוגה ומכיל כל ::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 במקום זאת.