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

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

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

ההחלפה (cross-fade) שמוגדרת כברירת מחדל. הדגמה מינימלית. מקור.

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


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

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

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

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

בסיום, תתבצע קריאה ל-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 {
    …
}

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

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

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

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


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

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

מיקום מוחלט בפינה הימנית העליונה של העטיפה.

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

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

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


משוב

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

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