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

פורסם: 17 באוגוסט 2021, עדכון אחרון: 25 בספטמבר 2024

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

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

לאחר מכן, התמונות האלה מסודרות בעץ של פסאודו-אלמנטים ומונפשות באמצעות האנימציות של 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 'replaced content' (כמו <img>).

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

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

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

אפשר להגדיר טירגוט לכל רכיבי ה-pseudo של מעבר התצוגה באמצעות 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

Browser Support

  • Chrome: 125.
  • Edge: 125.
  • Firefox Technology Preview: supported.
  • Safari: 18.2.

Source

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

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, ולכן החלונית Animations בכלי הפיתוח ל-Chrome מצוינת לניפוי באגים במעברים.

באמצעות החלונית Animations (אנימציות), אפשר להשהות את האנימציה הבאה, ואז להזיז את סרגל ההתקדמות קדימה ואחורה כדי לעבור בין האנימציות. במהלך המעבר, אפשר למצוא את רכיבי ה-pseudo בחלונית Elements.

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

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

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;
  }
}

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


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

Browser Support

  • Chrome: 125.
  • Edge: 125.
  • Firefox Technology Preview: supported.
  • Safari: 18.

Source

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

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

לשם כך, אפשר להשתמש בסוגי מעברים בין תצוגות, שמאפשרים להקצות סוג אחד או יותר למעבר פעיל בין תצוגות. לדוגמה, כשעוברים לדף גבוה יותר ברצף של מספור עמודים, משתמשים בסוג 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 {
    
}

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

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

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

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

זה הכול!

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

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


שילוב עם Navigation API (ומסגרות אחרות)

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

בקטע הקוד הבא, שנלקח מהדמו הזה של חלוקה לדפים, ה-handler של ה-interception של Navigation API מותאם לקריאה ל-document.startViewTransition כשיש תמיכה במעברי תצוגה.

navigation.addEventListener("navigate", (e) => {
    // Don't intercept if not needed
    if (shouldNotIntercept(e)) return;

    // Intercept the navigation
    e.intercept({
        handler: async () => {
            // Fetch the new content
            const newContent = await fetchNewContent(e.destination.url, {
                signal: e.signal,
            });

            // The UA does not support View Transitions, or the UA
            // already provided a Visual Transition by itself (e.g. swipe back).
            // In either case, update the DOM directly
            if (!document.startViewTransition || e.hasUAVisualTransition) {
                setContent(newContent);
                return;
            }

            // Update the content using a View Transition
            const t = document.startViewTransition(() => {
                setContent(newContent);
            });
        }
    });
});

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

לכן מומלץ למנוע את תחילת המעבר בין התצוגות כשהדפדפן מספק מעבר חזותי משלו. כדי לעשות את זה, בודקים את הערך של המאפיין hasUAVisualTransition של מופע NavigateEvent. המאפיין מוגדר לערך true כשהדפדפן מספק מעבר חזותי. המאפיין hasUIVisualTransition הזה קיים גם במופעים של PopStateEvent.

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

if (!document.startViewTransition || e.hasUAVisualTransition) {
  setContent(newContent);
  return;
}

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

השוואה בין אותו אתר בלי (מימין) ועם (משמאל) בדיקה של hasUAVisualTransition
.

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


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

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

  • React – המפתח כאן הוא flushSync, שמחיל קבוצה של שינויים במצב באופן סינכרוני. כן, יש אזהרה גדולה לגבי השימוש ב-API הזה, אבל Dan Abramov מבטיח לי שהשימוש בו מתאים במקרה הזה. כמו תמיד עם React וקוד אסינכרוני, כשמשתמשים בהבטחות השונות שמוחזרות על ידי 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

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

ה-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) של פסאודו-מחלקות בשורש המעבר.

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

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

מוצב באופן מוחלט בפינה השמאלית העליונה של רכיב העטיפה.

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

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

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


משוב

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

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