תאריך פרסום: 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
תמיכה בדפדפנים
נניח שיש לכם מעבר תצוגה עם כמה כרטיסים, אבל גם כותרת בדף. כדי להוסיף אנימציה לכל הכרטיסים מלבד הכותרת, צריך לכתוב סלקטורים שמטרגטים כל כרטיס בנפרד.
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)
.
ניפוי באגים במעברים
מכיוון שמעברי תצוגה מבוססים על אנימציות CSS, החלונית אנימציות ב-Chrome DevTools מתאימה במיוחד לניפוי באגים במעברים.
באמצעות החלונית Animations (אנימציות), אפשר להשהות את האנימציה הבאה ואז לעבור קדימה ואחורה באנימציה. במהלך התהליך, פסאודו-האלמנטים של המעבר יופיעו בחלונית Elements.
רכיבי המעבר לא חייבים להיות אותו רכיב 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;
}
}
אבל ההעדפה של 'תנועה מופחתת' לא אומרת שהמשתמש רוצה ללא תנועה. במקום קטע הקוד הקודם, אפשר לבחור אנימציה עדינה יותר, אבל עדיין כזו שמציגה את הקשר בין הרכיבים ואת זרימת הנתונים.
טיפול במספר סגנונות של מעברי תצוגה באמצעות סוגי מעברי תצוגה
תמיכה בדפדפנים
לפעמים צריך להשתמש בהעברה מותאמת אישית כדי לעבור מתצוגה מסוימת לתצוגה אחרת. לדוגמה, כשעוברים לדף הבא או לדף הקודם בסדרת דפים, כדאי להחליק את התוכן בכיוון שונה בהתאם לדף שאליו עוברים – דף גבוה יותר או דף נמוך יותר בסדרה.
לשם כך, אפשר להשתמש בסוגי מעברי תצוגה, שמאפשרים להקצות סוג אחד או יותר למעבר תצוגה פעיל. לדוגמה, כשעוברים לדף גבוה יותר ברצף החלוקה לדפים, צריך להשתמש בסוג 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
. ההקלטה בצד שמאל כוללת את הבדיקה, ולכן היא מדלגת על המעבר הידני לתצוגה כי הדפדפן סיפק מעבר חזותי.
אנימציה באמצעות 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 במקום זאת.