Tworzenie efektu paralaksy podczas przewijania

Funkcje internetowe: Animacje sterowane przewijaniem

Efekt paralaksy podczas przewijania to technika wizualna, w której różne warstwy treści poruszają się z różną szybkością, gdy użytkownik przewija stronę w dół. Tworzy to iluzję głębi, ponieważ elementy na pierwszym planie wydają się poruszać szybciej niż elementy w tle, co zapewnia angażujące i wciągające wrażenia podczas przeglądania. Najlepszy efekt można osiągnąć, używając animacji CSS opartych na przewijaniu, które umożliwiają powiązanie animacji z pozycją przewijania kontenera.

Jak to zrobić?

Aby utworzyć podstawowy efekt paralaksy:

  1. Utwórz element opakowujący: ten element po prostu grupuje wszystkie warstwy efektu paralaksy. Nie jest to element przewijany, więc jego przepełnienie powinno zostać przycięte. Ustaw też height, która odpowiada wysokości jednej z warstw efektu paralaksy.

    <div class="wrapper">
      …
    </div>
    
    .wrapper {
      overflow: clip;
      height: 100vh; /* Height of one of the layers of the parallax */
    }
    
  2. Zadeklaruj warstwy: w elemencie wrapper dodaj poszczególne warstwy, które będą się poruszać z różną szybkością.

    <div class="wrapper">
      <div class="layer">LAYER 0</div>
      <div class="layer">LAYER 1</div>
      <div class="layer">LAYER 2</div>
      …
    </div>
    
  3. Dodaj animację przesunięcia: zdefiniuj animację CSS, która zmienia właściwość transform warstw. Aby uzyskać efekt paralaksy, zwykle używa się właściwości translateY do przesuwania warstw w pionie.

    @keyframes parallax {
      from {
        transform: translateY(700px);
      }
    }
    
  4. Skonfiguruj view-timeline: aby połączyć animację z pozycją przewijania, utwórz view-timeline w elemencie opakowania, a następnie zastosuj go do warstw.

    .wrapper {
      view-timeline: --wrapper;
    }
    
    .layer {
      animation: parallax linear both;
      animation-timeline: --wrapper;
    }
    
  5. Przesunięcie animacji: aby warstwy poruszały się z różnymi prędkościami, możesz użyć jednego z 2 głównych podejść: przesunięcia w klatkach kluczowych lub przesunięcia animation-range.

    Obie metody mogą używać wartości zakodowanych na stałe lub implementacji sibling-index()/sibling-count(). Wartości zakodowane na stałe są najłatwiejsze i przydatne, gdy masz tylko ograniczoną liczbę warstw. Implementacja sibling-index()/sibling-count() jest przydatna, gdy masz wiele warstw.

    • Staggering in the keyframes:

      Za pomocą wartości zakodowanych na stałe możesz zdefiniować właściwość niestandardową dla każdej warstwy, aby ręcznie kontrolować jej przesunięcie paralaksy.

      .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));
        }
      }
      

      Za pomocą sibling-index() możesz sprawić, że funkcja sibling-index() będzie zwracać indeks elementu podrzędnego wśród jego elementów równorzędnych, aby automatycznie obliczać efekt kaskadowy.

      @keyframes parallax {
        from {
          transform: translateY(calc(100px * sibling-index()));
        }
      }
      
    • Staggering the animation-range:

      Za pomocą wartości zakodowanych na stałe możesz wyraźnie określić granice animation-range na każdej warstwie osobno.

      .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%; }
      

      Za pomocą sibling-index() i sibling-count() możesz obliczyć zakres matematycznie na podstawie łącznej liczby warstw (sibling-count()).

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

Przykładowy kod

@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;
  }
}

Możesz też użyć właściwości animation-range, aby uzyskać podobny efekt:

@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;
  }
}

Sprawdzone metody

Podczas korzystania z animacji sterowanych przewijaniem warto stosować kilka sprawdzonych metod, aby zapewnić płynne i dostępne działanie:

  • DO include feature detection: Not all browsers support scroll-driven animations. Użyj @supports ((animation-timeline: view()) and (animation-range: entry)), aby sprawdzić, czy jest obsługiwany, i zapewnić rozwiązanie zastępcze w przypadku przeglądarek, które go nie obsługują.
    • MUSISZ uwzględnić tutaj sprawdzenie (animation-range: entry), aby odfiltrować przeglądarki, które obsługują tylko częściowo.
    • NIE używaj pakietu scroll-timeline-polyfill w strategii zastępczej, ponieważ nie jest on kompletny i ma wiele znanych problemów.
    • Jeśli animacja ma charakter wyłącznie dekoracyjny, wybierz progresywne ulepszanie i NIE podawaj kreacji zastępczej.
  • DO respect user preferences: Some users prefer to have less motion on the web. Użyj zapytania o media prefers-reduced-motion, aby wyłączyć lub ograniczyć animacje dla tych użytkowników.
  • DO try to animate only performant CSS properties: For the smoothest animations, stick to animating properties that can be handled by the browser's compositor thread, such as transform and opacity. Animowanie innych właściwości, takich jak width lub height, może powodować problemy z wydajnością.
  • DO use the correct declaration order: When using the animation shorthand property, declare animation-timeline and animation-range after it to prevent the shorthand from resetting the timeline.

Jeśli chodzi o ustawienie animation-range:

  • DO give all layers the same start offset, e.g. entry 25%
  • DO give all layers a different end offset that uses sibling-count() and sibling-index() to distribute the offsets, e.g. exit calc(100% / sibling-count() * sibling-index()).

Obsługa przeglądarek i strategie zastępcze

Dlatego zwykle wymagana jest strategia zastępcza.

W przypadku przeglądarek, które nie obsługują animacji sterowanych przewijaniem, możesz użyć kreacji zastępczej, aby odtworzyć efekty wizualne. Kreacje zastępcze są zwykle tworzone za pomocą detektora przewijania (w przypadku efektów ScrollTimeline) lub interfejsu IntersectionObserver API (w przypadku efektów ViewTimeline).

W przeglądarkach z wbudowaną obsługą animacji opartych na przewijaniu ZAWSZE używaj natywnej implementacji CSS, ponieważ jest ona bardziej wydajna.

Pamiętaj, że nie każdy efekt można odtworzyć za pomocą kreacji zastępczych.

W tym konkretnym przypadku poniższy skrypt stosuje kreację zastępczą w przeglądarkach, które nie obsługują animacji sterowanych przewijaniem. Używa IntersectionObserver do śledzenia widoczności elementu .wrapper i aktualizuje właściwość transform warstw na podstawie pozycji przewijania.

// 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();
}