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