איך ליצור אפקט פרלקס בגלילה

תכונות לאינטרנט: אנימציות שמופעלות בגלילה

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

איך מטמיעים

כך יוצרים אפקט פרלקס בסיסי:

  1. יוצרים רכיב wrapper: הרכיב הזה פשוט מקבץ את כל השכבות של אפקט הפרלקס. זה לא הרכיב שניתן לגלילה, ולכן צריך לחתוך את החלק שחורג ממנו. צריך גם להגדיר height שתואם לגובה של אחת מהשכבות של אפקט הפרלקסה.

    <div class="wrapper">
      …
    </div>
    
    .wrapper {
      overflow: clip;
      height: 100vh; /* Height of one of the layers of the parallax */
    }
    
  2. הצהרה על השכבות: בתוך ה-wrapper, מוסיפים את השכבות הנפרדות שיזוזו במהירויות שונות.

    <div class="wrapper">
      <div class="layer">LAYER 0</div>
      <div class="layer">LAYER 1</div>
      <div class="layer">LAYER 2</div>
      …
    </div>
    
  3. הוספת אנימציית הזזה: מגדירים אנימציית CSS שמשנה את המאפיין transform של השכבות. כדי ליצור אפקט פרלקס, בדרך כלל משתמשים ב-translateY כדי להזיז את השכבות בצורה אנכית.

    @keyframes parallax {
      from {
        transform: translateY(700px);
      }
    }
    
  4. הגדרת view-timeline: כדי לקשר את האנימציה למיקום הגלילה, יוצרים view-timeline ברכיב ה-wrapper ואז מחילים אותו על השכבות.

    .wrapper {
      view-timeline: --wrapper;
    }
    
    .layer {
      animation: parallax linear both;
      animation-timeline: --wrapper;
    }
    
  5. הגדרת זמנים שונים לאנימציות: כדי שהשכבות יזוזו במהירויות שונות, אתם יכולים להשתמש באחת משתי שיטות עיקריות: הגדרת זמנים שונים בקיפריימים או הגדרת זמנים שונים בanimation-range.

    בשתי הגישות האלה אפשר להשתמש בערכים שמוגדרים בתוך הקוד, או בהטמעה של sibling-index()/sibling-count(). הערכים בתוך הקוד הם הכי קלים לשימוש, והם גם שימושיים כשיש רק מספר מוגבל של שכבות. ההטמעה של sibling-index()/sibling-count() שימושית כשיש הרבה שכבות.

    • הדרגתיות בקיפריימים:

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

      .layer:nth-child(1) { --offset: 100px; }
      .layer:nth-child(2) { --offset: 200px; }
      .layer:nth-child(3) { --offset: 300px; }
      
      @keyframes parallax {
        from {
          transform: translateY(var(--offset));
        }
      }
      

      באמצעות sibling-index(), הפונקציה sibling-index() מחזירה את האינדקס של רכיב צאצא מבין הרכיבים האחים שלו כדי לחשב באופן אוטומטי את האפקט המדורג.

      @keyframes parallax {
        from {
          transform: translateY(calc(100px * sibling-index()));
        }
      }
      
    • הדרגתיות בanimation-range:

      באמצעות ערכים בתוך הקוד, אתם יכולים להגדיר במפורש את הגבולות של animation-range בכל שכבה בנפרד.

      .layer:nth-child(1) { animation-range: entry 25% exit 50%; }
      .layer:nth-child(2) { animation-range: entry 25% exit 75%; }
      .layer:nth-child(3) { animation-range: entry 25% exit 100%; }
      

      בעזרת sibling-index() ו-sibling-count(), אתם יכולים לחשב את הטווח באופן מתמטי על סמך המספר הכולל של השכבות (sibling-count()).

      .layer {
        animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
      }
      

קוד לדוגמה

@keyframes parallax {
  from {
    transform: translateY(calc(100px * sibling-index()));
  }
}

.wrapper {
  view-timeline: --wrapper;
}

.layer {
  animation: parallax linear both;
  animation-timeline: --wrapper;
}

@media (prefers-reduced-motion: reduce) {
  .layer {
    animation: none;
  }
}

לחלופין, אפשר להשתמש במאפיין animation-range כדי להשיג אפקט דומה:

@keyframes parallax {
  from {
    transform: translateY(700px);
  }
}

.wrapper {
  view-timeline: --wrapper;
}

.layer {
  animation: parallax linear both;
  animation-timeline: --wrapper;
  animation-range: entry 25% exit calc(100% / sibling-count() * sibling-index());
}

@media (prefers-reduced-motion: reduce) {
  .layer {
    animation: none;
  }
}

שיטות מומלצות

כשמשתמשים באנימציות מבוססות-גלילה, חשוב לפעול לפי כמה שיטות מומלצות כדי להבטיח חוויה חלקה ונגישה:

  • מומלץ לכלול זיהוי תכונות: לא כל הדפדפנים תומכים באנימציות מבוססות-גלילה. כדאי להשתמש ב-@supports ((animation-timeline: view()) and (animation-range: entry)) כדי לבדוק אם יש תמיכה, ולספק חלופה לדפדפנים שלא תומכים בזה.
    • חובה לכלול כאן את הבדיקה של (animation-range: entry) כדי לסנן דפדפנים עם תמיכה חלקית בלבד.
    • אל תשתמשו בחבילה scroll-timeline-polyfill כאסטרטגיית גיבוי, כי היא לא כוללת את כל התכונות ויש בה הרבה בעיות ידועות.
    • אם האנימציה נחשבת רק לקישוט, כדאי לבחור בשיפור הדרגתי (progressive enhancement) ולא לספק חלופה.
  • חשוב לכבד את העדפות המשתמשים: חלק מהמשתמשים מעדיפים פחות תנועה באינטרנט. כדי להשבית או לצמצם את האנימציות למשתמשים האלה, צריך להשתמש בשאילתת המדיה prefers-reduced-motion.
  • מומלץ לנסות להנפיש רק מאפייני CSS עם ביצועים גבוהים: כדי שהאנימציות יהיו חלקות ככל האפשר, כדאי להנפיש רק מאפיינים שאפשר לטפל בהם בשרשור ה-compositor של הדפדפן, כמו transform ו-opacity. הנפשה של מאפיינים אחרים כמו width או height עלולה לגרום לבעיות בביצועים.
  • כן: משתמשים בסדר ההצהרה הנכון. כשמשתמשים במאפיין המקוצר animation, מצהירים על animation-timeline ו-animation-range אחריו כדי למנוע מהמאפיין המקוצר לאפס את ציר הזמן.

לגבי הגדרת animation-range:

  • מומלץ להגדיר לכל השכבות את אותו היסט התחלה, למשל entry 25%
  • מומלץ לתת לכל השכבות היסט סיום שונה שמשתמש ב-sibling-count() וב-sibling-index() כדי להפיץ את ההיסטים, למשל exit calc(100% / sibling-count() * sibling-index()).

תמיכה בדפדפנים ושיטות חלופיות

לכן, בדרך כלל נדרשת אסטרטגיית גיבוי.

בדפדפנים שלא תומכים באנימציות מבוססות-גלילה, אפשר להשתמש ב-fallback כדי ליצור מחדש את האפקטים החזותיים. החלופות בדרך כלל נוצרות באמצעות מאזין גלילה (לאפקטים של ScrollTimeline) או IntersectionObserver API (לאפקטים של ViewTimeline).

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

חשוב לזכור שלא כל אפקט אפשר ליצור מחדש באמצעות גישת החלופות.

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

// Fallback for browsers that don't support scroll-driven animations
if (!CSS.supports('(animation-timeline: view()) and (animation-range: entry)')) {
  const wrapper = document.querySelector('.wrapper');
  const layers = document.querySelectorAll('.layer');

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        window.addEventListener('scroll', onScroll);
      } else {
        window.removeEventListener('scroll', onScroll);
      }
    });
  }, { threshold: 0 });

  observer.observe(wrapper);

  function onScroll() {
    const scrollY = window.scrollY;
    const wrapperRect = wrapper.getBoundingClientRect();
    const wrapperTop = wrapperRect.top + scrollY;
    const wrapperHeight = wrapperRect.height;
    const windowHeight = window.innerHeight;

    if (scrollY >= wrapperTop - windowHeight && scrollY <= wrapperTop + wrapperHeight) {
      const scrollPercent = (scrollY - (wrapperTop - windowHeight)) / (wrapperHeight + windowHeight);
      
      layers.forEach((layer, index) => {
        // This matches the effect as defined in the CSS example above.
        // Customize this further if needed.
        const initialTranslateY = 100 * index;
        const translateY = initialTranslateY * (1 - scrollPercent);
        layer.style.transform = `translateY(${translateY}px)`;
      });
    }
  }

  // Trigger onScroll once to set initial positions
  onScroll();
}