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

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

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

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

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

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

תמיכה בדפדפנים

  • Chrome:‏ 125.
  • Edge: ‏ 125.
  • Firefox: לא נתמך.
  • Safari Technology Preview: יש תמיכה.

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

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 DevTools מתאימה במיוחד לניפוי באגים במעברים.

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

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

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

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

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


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

תמיכה בדפדפנים

  • Chrome:‏ 125.
  • Edge: ‏ 125.
  • Firefox: לא נתמך.
  • Safari: 18.

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

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

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

זה הכול!

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

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


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

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

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


מעברים כתכונה משופרת

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


עבודה עם frameworks

אם אתם עובדים עם ספרייה או framework שמסירים שינויי 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

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

ה-View Transition 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

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

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

יש לו mix-blend-mode: plus-lighter כדי לאפשר מעבר חלק בין שני הצלילים.

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


משוב

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

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