סיפור בגלילה הוא טכניקה פופולרית ליצירת חוויות מעניינות וסוחפות באינטרנט. היא כוללת הנפשה של אלמנטים בדף בזמן שהמשתמש גולל, וכך מספרת סיפור או מנחה את המשתמש לאורך הנרטיב. בעזרת אנימציות מבוססות-גלילה ב-CSS, אתם יכולים ליצור את האפקטים האלה ישירות ב-CSS, בלי להסתמך על JavaScript. האנימציות נשלטות על ידי מיקום הגלילה, ולא על ידי שעון מבוסס-זמן, מה שמבטיח שהן תמיד מסונכרנות עם הגלילה של המשתמש.
איך מטמיעים
כדי ליצור חוויית scrollytelling, צריך שתי קבוצות של רכיבים: אחת למעקב אחרי מיקום הגלילה ואחת להנפשה.
קודם כול, מגדירים view-timeline עם שם ברכיבים שרוצים לעקוב אחריהם. הם ישמשו כגורמים שמפעילים את האנימציות.
#tracked {
section:nth-child(1){ view-timeline: --tl-1 block; }
section:nth-child(2){ view-timeline: --tl-2 block; }
section:nth-child(3){ view-timeline: --tl-3 block; }
section:nth-child(4){ view-timeline: --tl-4 block; }
section:nth-child(5){ view-timeline: --tl-5 block; }
}
בשלב הבא, מוסיפים אנימציות לרכיבים שרוצים להנפיש ומקשרים אותם לציר הזמן שיצרתם באמצעות המאפיין animation-timeline.
#animated {
section {
animation: animate-in auto linear both, animate-out auto linear forwards;
animation-range: entry 25% cover 50%, exit 50% exit 75%;
}
section:nth-child(1){ animation-timeline: --tl-1; }
section:nth-child(2){ animation-timeline: --tl-2; }
section:nth-child(3){ animation-timeline: --tl-3; }
section:nth-child(4){ animation-timeline: --tl-4; }
section:nth-child(5){ animation-timeline: --tl-5; }
}
כדי ש-animation-timeline יוכל להפנות לציר הזמן בעל השם, הם צריכים להיות באותו היקף. אפשר להשתמש במאפיין timeline-scope באב קדמון משותף כדי להפוך את צירי הזמן לזמינים לכל הרכיבים שצריכים אותם. הרכיב :root הוא בדרך כלל בחירה טובה למטרה הזו.
html {
timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}
לבסוף, אתם יכולים להשתמש במאפיין animation-range כדי לציין את הטווח המדויק של ציר הזמן שבו האנימציה אמורה לפעול. ככה אתם יכולים לשלוט במדויק מתי האנימציות מופעלות ואיך הן מתקדמות.
#animated section {
animation-range: entry 25% cover 50%, exit 50% exit 75%;
}
קוד לדוגמה
html {
timeline-scope: --tl-1, --tl-2, --tl-3, --tl-4, --tl-5;
}
#tracked {
section:nth-child(1){ view-timeline: --tl-1 block; }
section:nth-child(2){ view-timeline: --tl-2 block; }
section:nth-child(3){ view-timeline: --tl-3 block; }
section:nth-child(4){ view-timeline: --tl-4 block; }
section:nth-child(5){ view-timeline: --tl-5 block; }
}
@keyframes animate-in {
from { scale: 0.5; opacity: 0; transform: rotateY(-180deg); }
to { transform: rotateY(0deg); }
}
@keyframes animate-out {
to { translate: 100% 0; opacity: 0; }
}
#animated {
section {
animation: animate-in auto linear both, animate-out auto linear forwards;
animation-range: entry 25% cover 50%, exit 50% exit 75%;
backface-visibility: hidden;
}
section:nth-child(1){ animation-timeline: --tl-1; }
section:nth-child(2){ animation-timeline: --tl-2; }
section:nth-child(3){ animation-timeline: --tl-3; }
section:nth-child(4){ animation-timeline: --tl-4; }
section:nth-child(5){ animation-timeline: --tl-5; }
}
/* MANDATORY Copy-Paste Safety: Disable continuous storytelling motion for sensitive users */
@media (prefers-reduced-motion: reduce) {
#animated section {
animation: none !important;
opacity: 1 !important;
transform: none !important;
}
}
שיטות מומלצות
כשמשתמשים באנימציות מבוססות-גלילה, חשוב לפעול לפי כמה שיטות מומלצות כדי להבטיח חוויה חלקה ונגישה:
- מומלץ לכלול זיהוי תכונות: לא כל הדפדפנים תומכים באנימציות מבוססות-גלילה. כדאי להשתמש ב-
@supports ((animation-timeline: scroll()) and (animation-range: 0% 100%))כדי לבדוק אם יש תמיכה, ולספק חלופה לדפדפנים שלא תומכים בזה.- חובה לכלול כאן את הבדיקה של
(animation-range: 0% 100%)כדי לסנן דפדפנים עם תמיכה חלקית בלבד. - אל תשתמשו בחבילה
scroll-timeline-polyfillכאסטרטגיית גיבוי, כי היא לא כוללת את כל התכונות ויש בה הרבה בעיות ידועות. - אם האנימציה נחשבת רק לקישוט, כדאי לבחור בשיפור הדרגתי (progressive enhancement) ולא לספק חלופה.
- חובה לכלול כאן את הבדיקה של
- חשוב לכבד את העדפות המשתמשים: חלק מהמשתמשים מעדיפים פחות תנועה באינטרנט. כדי להשבית או לצמצם את האנימציות למשתמשים האלה, צריך להשתמש בשאילתת המדיה
prefers-reduced-motion. - מומלץ לנסות להנפיש רק מאפייני CSS עם ביצועים גבוהים: כדי שהאנימציות יהיו חלקות ככל האפשר, כדאי להנפיש רק מאפיינים שאפשר לטפל בהם בשרשור ה-compositor של הדפדפן, כמו
transformו-opacity. הנפשה של מאפיינים אחרים כמוwidthאוheightעלולה לגרום לבעיות בביצועים. - כן: משתמשים בסדר ההצהרה הנכון. כשמשתמשים במאפיין המקוצר
animation, מצהירים עלanimation-timelineו-animation-rangeאחריו כדי למנוע מהמאפיין המקוצר לאפס את ציר הזמן.
כשמשתמשים במאפיין view-timeline כדי ליצור אנימציה שמופעלת בגלילה:
- מומלץ להשתמש ב-
<dashed-ident>של CSS בשם. - אופציונלי: מציינים במפורש את הציר למעקב: כשלא מטרגטים את ציר ברירת המחדל
block(למשל, בסרגל גלילה אופקי), מציינים במפורש את הציר למעקב באמצעותview-timeline-axis. - חשוב לוודא שהיקף החיפוש פועל: כשהרכיב שמצהיר על
view-timelineהוא לא רכיב אב בעץ שטוח של הרכיב המונפש, צריך להגדיל את החשיפה של השם שלview-timelineבאמצעותtimeline-scopeברכיב אב משותף.
אסטרטגיות חלופיות
בדפדפנים שלא תומכים באנימציות מבוססות-גלילה, אפשר להשתמש ב-fallback כדי ליצור מחדש את האפקטים החזותיים. החלופות בדרך כלל נוצרות באמצעות מאזין גלילה (לאפקטים של ScrollTimeline) או IntersectionObserver API (לאפקטים של ViewTimeline).
בדפדפנים עם תמיכה מובנית באנימציות מבוססות-גלילה, תמיד צריך להשתמש בהטמעה המקורית של CSS כי היא יעילה יותר.
חשוב לזכור שלא כל אפקט אפשר ליצור מחדש באמצעות גישת החלופות.
במקרה הספציפי הזה, הסקריפט הבא מחיל את החלופה לדפדפנים שלא תומכים באנימציות מבוססות-גלילה. הוא משתמש ב-IntersectionObserver כדי לעקוב אחרי החשיפה של כל רכיב #tracked section ומעדכן את המאפיין transform של ה-#animated section המתאים בהתאם.
const animatedSections = document.querySelectorAll('#animated section');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const sectionIndex = Array.from(document.querySelectorAll('#tracked section')).indexOf(entry.target);
if (sectionIndex !== -1) {
const animatedSection = animatedSections[sectionIndex];
const ratio = entry.intersectionRatio;
// Animate-in
animatedSection.style.opacity = ratio;
animatedSection.style.transform = `scale(${0.5 + ratio * 0.5}) rotateY(${-180 + ratio * 180}deg)`;
// Animate-out
if (ratio < 0.5) {
animatedSection.style.translate = `${(0.5 - ratio) * 2 * 100}% 0`;
} else {
animatedSection.style.translate = '0 0';
}
}
});
}, { threshold: Array.from({length: 101}, (_, i) => i / 100) });
document.querySelectorAll('#tracked section').forEach(section => {
observer.observe(section);
});
וה-CSS הנלווה:
#animated section {
opacity: 0;
transform: scale(0.5) rotateY(-180deg);
backface-visibility: hidden;
}
/* MANDATORY Copy-Paste Safety: Ensure content remains fully visible and legible for assistive technologies or users with motion sensitivities */
@media (prefers-reduced-motion: reduce) {
#animated section {
opacity: 1 !important;
transform: none !important;
translate: 0 0 !important;
}
}
החלופה הזו מספקת אנימציה מדויקת יותר שמופעלת בגלילה בדפדפנים שלא תומכים בתכונת ה-CSS המקורית, וכך מבטיחה חוויה עקבית יותר לכל המשתמשים. באמצעות סדרה של ערכי סף ל-IntersectionObserver, אנחנו יכולים לעקוב אחרי מיקום הגלילה בצורה מדויקת יותר וליצור אנימציה חלקה יותר.