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

💥 תבנית העיצוב: OV והפאזל
OV (overload) הוא הכשל הנפוץ באתרי פרימיום: אנימציות כבדות, נעילות גלילה (ScrollTrigger.pin) ושרשור של timelines.
- עייפות: גלילה שנעצרת ותנועה עודפת מעייפות.
- ספגטי של timelines: A נכנס, B זז, C משתנה. כל שינוי בתוכן מחייב לחשב מחדש את הזמנים.
- ביצועים: חישובים בזמן גלילה, בעיקר בטלפון.
התשובה, שלושה חוקים:
- sticky ולא pin: נעילה ב-
position: stickyשל הדפדפן, בלי נעילה ב-JS (נעילה בלי pin). - העימוד הוא המצב הסופי: ברוב המקרים ה-DOM בנוי בעיצוב הסופי של הדף, והאנימציה רק מובילה לשם. אפקט לא מחזיק עיצוב משלו במספרים של JS, ולכן הוא רספונסיב מעצמו (הפאזל,
data-from, הוא דוגמה אחת). - מצב קריאה: המבנה והאנימציה נפרדים, אז דגל אחד מבטל את ה-
from, והאתר חוזר לסטטי, קריא ונגיש. ROADMAP
| 💥 OV | מה | החוק |
|---|---|---|
| 0 | בלי אנימציה בכלל: גלילה רגילה. | גם זו בחירה: ברמת המנגנון יש מקרים שפשוט אין בהם אנימציה. |
| 1-3 | תוכן שקט: טיפוגרפיה, מרווחים, כניסות. | הנשימה של הדף. |
| 4-6 | פאזל ומורף: אלמנט אחד שעובר מ-A ל-B. תמונה במסך מלא שנוחתת בכרטיס בגריד, תמונה שמתפרקת ל-30 ריבועים ומתחברת. | פעם אחת, מ-A ל-B. |
| 7-10 | סצנה קולנועית: כמה שלבים. רכב נכנס, חלון נפתח, מלל נחשף. | אחת לדף. במצב קריאה רק הפריים האחרון. |
סובייקטיבי: זה לא משהו מתמטי שמודדים, זו גם תחושה. הציונים לפי קטגוריה במקור אחד, app/docs/gsap/_ov.ts, ומוצגים ב-GSAP ובראש כל דף קטגוריה. הם מצטברים: ה-OV של סקשן הוא כל מה שנראה יחד באותו FOLD, עם תקרה של 10.
מנוחה: אחרי אפקט ב-OV גבוה בא אזור ב-OV נמוך. ככה הקורא נח, וזה האיזון בין סקשנים, לא רק בתוך FOLD אחד. למשל: כניסה מעניינת של HERO, ואחריה פשוט גלילה עם מלל רץ.
כמות התוכן: יחס הפוך. אפקט ב-OV גבוה לא מתאים להרבה תוכן, והרבה תוכן הולך עם OV נמוך.
| 💥 OV | כמה תוכן |
|---|---|
| 1 | כתבה בבלוג |
| 10 | FOLD שיש בו רק כותרת ענקית: "Edge Technology" |
עקרונות עבודה, לא כלל חלוט: המדדים מתיישרים זה עם זה (OV גבוה, ופולד כמעט ריק), אז יש פה מטריצה כמכוון. זו לא מתמטיקה בכל מצב.
גם עיצוב סטטי: OV מושפע בסוף גם מעיצוב בלי תנועה. דף שדוחפים בו הרבה Outside the Box מעמיס גם בלי אנימציה. את זה לא מודדים במטריצה: זה עיצוב.
מה זז ומה נקרא: על אלמנט שזז קשה יותר לקרוא מלל. לכן אצלנו התמונה זזה והמלל עומד, למשל בזיגזג.
| מה | דוגמה | התנועה |
|---|---|---|
| אווירה | אובייקט דקורטיבי, צל, אלמנט רקע, טקסט ענק ברקע | חופשית. זה וייב, אף אחד לא מנסה לקרוא אותו |
| כרטיסים ומורף | כרטיס לחיץ, עטיפה של גריד, תמונה שמפרידה בין סקשנים | רק במעבר. כשהוא נעצר, הוא עומד |
| קריאה | רשימת תוכן, תפריט, פסקה מרכזית | בלי תנועה |
הקשר הדף: בדף בית הגולש מחפש אווירה, ותמונת מוצר יכולה לזוז. ברשימה, כמו קטלוג או מחירון, הוא במשימה: סורק, משווה ולוחץ. שם אין תנועה בגלילה על הכרטיסים, רק hover.
DSL ל-AI: שפה מצומצמת שמודל בונה בה דף מאוזן. בלי יחס קבוע: האיזון נקבע מהסקשן הנוכחי, הקודם והבא. זה חישוב מורכב יותר, אבל הוא קורה מעצם הבנייה.
Nielsen Norman Group, Scrolljacking 101: ברוב המבחנים המשתתפים התבלבלו לפחות מעט כשהאתר השתלט על הגלילה, ומי שבא למשימה סבל את זה פחות ממי שסתם גלש.
WCAG 2.1, 2.3.3 Animation from Interactions: אנימציה שמופעלת מאינטראקציה, כמו גלילה, צריכה אפשרות לכבות אותה.
עצור פקח: בעיית ה-STOP של אפקטי 100VH
משהו עולה מעליו בזמן שהוא עומד: הבעיה כמעט נעלמתאפקט שתופס מסך, נגמר, ואז נגלל החוצה כבלוק אומר לקורא "יש עכשיו אפקט יפה של 100VH, תסיים אותו, מתחת יש תוכן רגיל". הפס התחתון של התמונה הוא הרגע שזה נחשף. לכן הם יפים ובקושי בשימוש.
בעיה צהובה: לא באג ולא "אל תעשה". באתר בעיצוב רגיל לרוב לא שמים באמצע הדף תמונת רקע בגובה מסך, והאפקטים האלה כמעט מחייבים אותה. הלכה למעשה, לא משנה איך זה מתחלף וכמה זה מרשים, השימושיות של האפקט נמוכה יותר, וזה ניכר גם בשימוש הנמוך בהם בדמואים או באתר הסטודיו. בעיצוב ממוצע, ללקוח ממוצע, זה כמעט לא שמיש.
הבעיה המרכזית: מה שהם נגמרים בו הוא תמונה עם שתי כותרות, עיצוב של hero. בלי האנימציה אף אחד לא היה שם סקשן כזה באמצע דף, ומאחוריהם אי אפשר לשים סקשן רגיל ברף מקצועי, מלל ליד תמונה. אפקט שנכנס לאתר רגיל הוא אפקט שהמצב הסופי שלו הוא עימוד רגיל, כמו Morph שנוחת ליד תוכן או הפאזל.
המבחן: לעצור בפריים שאחרי סוף האפקט. קו ישר של תמונה שיוצאת? זו עצירה. הקו עצמו לא הבעיה: בין שני אזורים של גלילה רגילה, קצה של צבע הוא סתם קצה.
| הדרך | מה עושים | חי ב |
|---|---|---|
| Rise Over | הסקשן הבא עולה בזמן שהתמונה עומדת, והיא מכוסה לפני שהיא עוזבת. הפשוטה מכולן. UkPushUnder | Push Under |
| One Ground | ל-pane ולסקשן הבא אותו צבע, והסקשן עולה מעל סוף המסילה. margin-top: -30vh | Eye Open |
| Staying World | התמונה עומדת לאורך כל התוכן, והוא נגלל מעליה. או שהיא ממשיכה הלאה כאזור פרלקס גדול. UkShapeWorld | Shape World |
| Fade to Black | האפקט נגמר לפני סוף המסילה, התמונה דועכת, והעמידה משתחררת בלי שרואים. wipeEnd | Before / After |
| Window in Fold | האפקט הוא חלון ליד מלל, על רקע הדף, ולא מסך מלא. כשהוא נגמר נגללת תמונה בתוך דף, והעין לא רואה שהוא עצר. uk-container | Morph |
// the plainest way: one wrapper, the effect itself stays as it is
<UkPushUnder rise="6vh" scaleTo={1} opacityTo={0.6}>
<UkPanelStack items={worlds} />
</UkPushUnder>
<section>rises over it while it still holds</section>Window in Fold, ב-ayocin.com: בזמן האנימציה, ואחריה בגלילה רגילה.


- הגלילה אף פעם לא נעצרה. רק ה-pane עומד, והסקשן שעולה זז במהירות רגילה. כשהוא מכסה את המסך נשארת גלילה רגילה.
- משהו מבצבץ לפני הסוף, וזה תוכן רגיל בכל עימוד, לא מלל באמצע.
- רווח ענק לא נקרא כרווח כשמשהו נכנס וזז. בדף סטטי אותו מרחק היה באג. רק בזמן תנועה: ברגע שה-pane משתחרר, הרווח כן נראה (בעיית המרחק).
- לא עוזר: להוריד את ה-sticky (הקו נעלם וגם הסיבה לאפקט), ומלל שכבר עומד מתחת לאפקט שנפתח (הקורא לא מבין מה קרה).
- גם בלי הפס יש שני מחירים: יש בו פריימים שלקוח לא יאשר אם יעצור עליהם (לוגו קטן על מסך לבן), והוא פוגע בשאיפה שבכל רגע של גלילה משהו באתר מגיב.
- לא בכל אפקט. תנועה בכיוון אחר מהעלייה נותנת רק חצי כוח, כמו ה-wipe הצידה של Before / After.
בעיית המרחק: הרגע שהסטיקי משתחרר
ה-pane ננעל, המצב הסופי שלו קטן (כרטיס באמצע המסך), והוא משתחרר רק 100svh אחר כך. נשאר מסך של קרקע ריקה לפני התוכן, ובמנוחה העין מודדת אותו. הלקוח יגיד "יא מלא רווח": בשבילו זה סתם רווח, וזה שהאפקט היה צריך אותו זו לא סיבה שהוא רואה.
איך מודדים: לא מקצה ה-pane ולא לפי ה-padding של הסקשן, אלא מתחתית הדבר האחרון שהפולד הסטיקי מראה (הכותרת שנכנסה בזום, הכרטיס) עד השורה הראשונה של הסקשן הבא. המרחק הזה צריך להיראות כמו רווח רגיל בין שני בלוקים בדף סטטי.
| הדרך | מה עושים |
|---|---|
| תמונת רקע לכל הסקשן | הרווח נראה קטן: העין מחשבת את המרחק מסוף התמונה, לא מהשורה האחרונה. |
| משהו עולה מעל, בסנכרון | הסקשן הבא עולה מעל סוף המסילה בזמן שה-pane עוד עומד, כך שהרווח לא נראה במנוחה. מתאים כשהסצנה נגמרת בקרקע שאפשר לבוא מעליה (כוכבים, עולם). כשהיא נגמרת באובייקט (כרטיס, מוצר), התוכן בא מתחתיו: UkMorph tight. |
| למלא את הסצנה בעיצוב | מספיק בסצנה הסטיקית שהגולש לא ירגיש את המרחק. למשל חתימה גדולה שחוצה את תחתית הכרטיס, וקווי קונטור חלשים שעוברים מתחת להירו ולסקשן שאחריו. |
חישוב בקוד: JS מודד את המרחק
המנגנון, tight: UkMorph מודד איפה נגמר התוכן של העימוד האחרון ולוקח בחזרה בדיוק את הקרקע הריקה שמתחתיו. הסקשן הבא מתחיל ממש מתחת לכרטיס, במרחק ה-padding שלו. בלי מספר קבוע: כרטיס גדול יותר, או כותרת מתחתיו, והמדידה זזה איתם.
// the next section rises over the ground and stops at the foot of the card
<UkMorph src={portrait} swap={note} tight>…</UkMorph>
<section className="my-next">…</section> /* a ground of its own, its top fading in */
.my-next { background: linear-gradient(to bottom, transparent, var(--ink) 32vh); }- מה מחושב ומה לא. tight הוא חישוב בקוד: JS מודד את התוכן של העימוד האחרון, ושוב בכל refresh של ScrollTrigger (שינוי גודל מסך). One Ground עם margin-top שלילי הוא CSS נקי בלי מדידה, ולכן הוא תלוי במספר שמישהו כתב.
- לא מספר קבוע. margin-top של -40vh על הסקשן הבא עבד רק לתמונה הזו בגודל הזה. מחר התמונה משתנה, או שיש מלל מתחתיה, וזה נשבר.
- בלי קו ישר. הסקשן שעולה חותך את מה שעומד בקצה ישר, וזו עצירה. הרקע שלו נכנס בהדרגה משקוף, וגם כל מה שבתוכו (זוהר, טקסטורה) נכנס איתו בהדרגה, אחרת קצה הקופסה חותך אותו לקו.
- השכבות לפי סדר ה-DOM. מה שבא אחרי עולה מעל, כי UkMorph סגור בתוך עצמו (isolation). בלי זה ה-z-index של התמונה בתוכו יצא החוצה והתמונה ישבה מעל הסקשן שעולה.
Mister Potato
mister_potato
סצנה היא שלושה חלקים שמתחלפים, כמו בצעצוע. אותו כיוון ושלוש מהירויות נותנים את ה-WOW, והמכפלה נותנת את המגוון. הסמן הזה יושב על אפקט שמורכב מחלקים של הארסנל בסניפט רזה, בלי קומפוננטה משלו, ומשווים אותו בעין מול קומפוננטה סגורה.
| החלק | מהירות | אפשרויות |
|---|---|---|
| כניסת הקלף | מהיר | UkPanelStack · UkPushUnder · uk-uncover · Cover Hero |
| תנועת הרקע | איטי | ParallaxImage · data-movement · data-scale |
| כניסת המלל | באמצע | trigger_box · split · split_chars · split="blur" · split="fill" |
החלק שעוד לא מתחלף עד הסוף הוא המלל: כניסה לפי in view לא מתאימה לתוכן שיושב ב-pane דביק. שם המלל צריך לרוץ על הטיימליין של הסצנה.
נעילה בלי pin
נראה פשוט כמו הגלילה הנייטיב של הדפדפן: אפקט יותר יציבה-CSS מחזיק את המסך, ו-GSAP רק מזיז דברים. מסילה גבוהה נותנת מרחק גלילה, pane דביק בתוכה נשאר על המסך, ו-ScrollTrigger ממיר את מעבר המסילה ל-0 עד 1. בלי pin: true. לעין זו אותה תוצאה.
/* 1. the track: 300vh, so the screen stands for 200vh of scroll */
.scene { height: 300vh; }
/* 2. the pane: the browser sticks it, no script. Under a bar that stays: --uk-sticky-top */
.scene__pane {
position: sticky;
top: var(--uk-sticky-top, 0px);
height: calc(100vh - var(--uk-sticky-top, 0px));
overflow: clip;
}
<section className="scene">
<div className="scene__pane">
<div className="scene__box">…</div>
</div>
</section>
// 3. GSAP only counts: the track crossing the screen is the time. No pin
const track = document.querySelector('.scene')
const pane = track.querySelector('.scene__pane')
gsap.timeline({
scrollTrigger: {
trigger: track,
start: () => `top ${parseFloat(getComputedStyle(pane).top) || 0}px`, // when the pane locks
end: 'bottom bottom', // when the track lets go
scrub: true,
invalidateOnRefresh: true,
},
defaults: { ease: 'none' },
})
.from('.scene__box', { y: 200, duration: 0.5 }, 0) // from 0 to 0.5 of the track
.to('.scene__box', { rotate: 180, duration: 0.3 }, 0.6) // from 0.6 to 0.9
.to({}, { duration: 0.1 }, 0.9) // pads it to 1: a timeline of 0.9 stretches over the whole track| pin: true | sticky | |
|---|---|---|
| מי מחזיק את המסך | JS: הופך ל-fixed ומחשב מיקום | הדפדפן |
| מה קורה ל-DOM | עוטף ב-pin-spacer ומוסיף גובה | כלום, המבנה כתוב ב-HTML |
| מרחק הגלילה | ב-JS: end: '+=2000' | גובה המסילה ב-CSS |
| ברגע הנעילה | עלולה להיות קפיצה | אין קפיצה |
| המלכודת | pin-spacer מול React במעבר דף | overflow: hidden או position על pane או על הורה מבטלים בשקט |
Puzzle on Scroll
ציר זמן של GSAP, כתוב על האלמנטים. מסילה שעוצרת, ובתוכה כל אלמנט אומר מאיפה הוא בא, מתי ולכמה זמן, מ-0 עד 1 מהמסילה. גללו: הקוביות נכנסות לפי הסדר רק כי data-at עולה, והפס בכל קובייה מראה את החלק שלה במסילה.
טיימליין של GSAP שכתוב על האלמנטים: לכל חלק זמן פתיחה ואורך יחסיים, על ציר אחד משותף. הלופ בונה מהם טיימליין רגיל, והגלילה היא ראש הניגון.
| מדרגה | מתי | יתרון | חיסרון |
|---|---|---|---|
| 1. אטריביוט עם טוקן | אפקט אחד שחוזר על הרבה אלמנטים | נקי מאוד למשתמש: ערך אחד, כמו scale-from="0.3". אצלנו data-scale, trigger_box | לשרשר איתו אנימציות על ציר הזמן לא ישים |
| 2. TL באטריביוטיםהגישה פה | סצנה שבה כל אלמנט זז פעם אחת | איזון בין גמישות לחזרתיות: הכול על האלמנט, בלי JS לכל סצנה. העימוד הוא המצב הסופי | from אחד לאלמנט, ובלי שרשור יחסי. כתיבה מעצבנת לאדם, עובד כש-AI כותב |
| 3. TL קלאסי | אלמנט שזז יותר מפעם אחת, שרשור ארוך. נדיר מאוד, בבקשה מפורשת ואפיון | חופש מוחלט. מתאים גם לאנימציה של 30 סצנות | ידני לחלוטין. לא עובד בגישת אטריביוטים |
כל סצנה בוחרת מדרגה, וגם באותו פרויקט אפשר לשלב. סצנת TL קלאסי עומדת לבד ולא קשורה למנגנון הזה. דוגמה: רכב נכנס למרכז המסך, החלונות נפתחים, האורות נדלקים, הרכב נוסע אחורה, ונוסע קדימה בזמן שהחלון נסגר. יש פה יותר מדי קשרים בין התנועות, אז זה ציר אנימציה מעצם היותו.
data-puzzle-piecedata-from="y:120%"data-at="0"data-for="0.25"0 + 0.25 = 0.25data-puzzle-piecedata-from="x:-120%"data-at="0.2"data-for="0.25"0.2 + 0.25 = 0.45data-puzzle-piecedata-from="scale:0 rotate:-180"data-at="0.4"data-for="0.25"0.4 + 0.25 = 0.65data-puzzle-piecedata-from="y:-120% opacity:0"data-at="0.6"data-for="0.3"0.6 + 0.3 = 0.9| אטריביוט | מה הוא מייצג | ערכים |
|---|---|---|
| מבנההמנוע שגורם לאפקט להיראות כמו pin: true | ||
data-puzzle-track | המסילה: מרחק הגלילה של הסצנה. 0 הוא הרגע שה-pane ננעל, 1 הוא הרגע שהמסילה משחררת אותו | אורך: --uk-puzzle-distance (ברירת מחדל 150svh) |
data-puzzle-pane | קבוע בכל סצנה: נדבק למסך. לא משפיע על האנימציה | בלי ערך. בלי position על האלמנט |
data-puzzle-mobile | על המסילה: רץ גם מתחת ל-768px. בלעדיו, בטלפון החלקים פשוט עומדים במקום | בלי ערך |
| גישת TLידני: כל חלק הוא שורה בטיימליין אחד, עם ערכים משלו. המנגנון from / to | ||
data-puzzle-piece | הלופ עובר על האטריביוט הזה: בלעדיו אלמנט לא נכנס ללופ, ו-data-from עליו לא עושה כלום. תנועה אחת לחלק, יותר מזה TL קלאסי | בלי ערך |
data-from | מאיפה החלק מגיע. הוא תמיד נגמר במקום שה-CSS נותן לו | x, y: בפיקסלים או ב-% מהגודל שלו. scale, rotate (מעלות), opacity, וכל תכונה אחרת של GSAP. כמה ערכים עם רווח, ולכן בלי רווח בתוך ערך |
data-to | פאזל הפוך: החלק מתחיל במקום שה-CSS נותן לו, והגלילה מפרקת אותו. על חלק אחד או זה או data-from | אותם ערכים כמו data-from |
data-at | מתחיל ב-X אחוז מהטיימליין כולו | 0 עד 0.99. למשל 0.6: מתחיל אחרי 60% מהגלילה. ברירת מחדל 0 |
data-for | נמשך X אחוז מהגלילה של הטיימליין כולו. לא שניות | 0 עד 1. למשל 0.3: נמשך 30% מהגלילה. ברירת מחדל: עד הסוף |
המשוואה: data-at + data-for היא הנקודה במסילה שבה החלק נוחת. קובייה 4: 0.6 + 0.3 = 0.9, כלומר מתחילה ב-60% ונוחתת ב-90%. סכום מעל 1 נחתך בסוף המסילה: data-for="200" הוא פשוט "עד הסוף".
<section data-puzzle-track="" className="[--uk-puzzle-distance:200svh]">
<div data-puzzle-pane="">
<img data-puzzle-piece="" data-from="y:85%" data-at="0.1" data-for="0.46" />
</div>
</section>
// puzzle_on_scroll.js: each piece is one line of a timeline tied to the scroll
tl.from(piece, { yPercent: 85, duration: 0.46 }, 0.1)- שכבות: חלקים שנבנים למוצר יושבים
absolute; inset: 0על אותו קנבס. ההדרגה היאdata-atשונה, עם חפיפה. - בלי
positionעל ה-pane, גם לאrelative: זה מבטל את ה-sticky בשקט.
המסילה וה-pane
אותה נעילה כמו למעלה, במונחים של הפאזל ובשלושה רגעים של המסילה:
הירידה של המסך במסילה קיימת רק בציור: היוזר תמיד רואה את ה-pane באותו מקום על המסך, והמסילה עוברת מאחוריו.
הלופ
הסקריפט עובר בלולאה על כל מסילה, ובתוכה על כל חלק, ולכן כמה סצנות בעמוד לא מתערבבות:
// puzzle_on_scroll.js, shortened
document.querySelectorAll('[data-puzzle-track]').forEach((track) => {
const tl = gsap.timeline({ scrollTrigger: { trigger: track, scrub: true } })
// the loop: data-puzzle-piece says who is in it, the other attributes are only read
track.querySelectorAll('[data-puzzle-piece]').forEach((piece) => {
const at = parseFloat(piece.dataset.at) || 0
const dur = Math.min(parseFloat(piece.dataset.for) || 1, 1 - at)
tl.from(piece, { ...parseFrom(piece.dataset.from), duration: dur }, at)
})
if (tl.duration() < 1) tl.to({}, { duration: 1 - tl.duration() }) // the timeline is always 1
})פאזל הפוך: data-to
אותם ערכים, בכיוון השני. ב-DOM החלקים כבר במקום הסופי, והגלילה מפרקת אותם. בלי JS ובטלפון בלי data-puzzle-mobile הם נשארים מורכבים.
data-puzzle-piecedata-to="y:120%"data-at="0"data-for="0.25"0 + 0.25 = 0.25data-puzzle-piecedata-to="x:-120%"data-at="0.2"data-for="0.25"0.2 + 0.25 = 0.45data-puzzle-piecedata-to="scale:0 rotate:-180"data-at="0.4"data-for="0.25"0.4 + 0.25 = 0.65data-puzzle-piecedata-to="y:-120% opacity:0"data-at="0.6"data-for="0.3"0.6 + 0.3 = 0.9איפה זה בשימוש (נכון ל-1.11)
- /demos/vision_31_10: VISION: חלקי המשקף
- /services/animated-websites: דף שירות: הטלפונים (TypePuzzle) והריבועים (TypeTiles)
- /beta/from-test: מעבדה זמנית: שתי תנועות על אלמנט אחד
