Как создать эффект параллакса при прокрутке

Эффект параллакса при прокрутке – это визуальный прием, при котором разные слои контента перемещаются с разной скоростью, когда пользователь прокручивает страницу вниз. Это создает иллюзию глубины, когда элементы переднего плана кажутся движущимися быстрее, чем элементы фона, что приводит к увлекательному и захватывающему просмотру. Этого эффекта лучше всего достичь с помощью анимации на основе прокрутки CSS, которая позволяет связать анимацию с положением прокрутки контейнера.

Рекомендации по внедрению

Вот как создать базовый эффект параллакса:

  1. Создайте элемент-контейнер. Он объединяет все слои эффекта параллакса. Это не прокручиваемый элемент, поэтому его переполнение должно быть обрезано. Также задайте для него значение height, соответствующее высоте одного из слоев эффекта параллакса.

    <div class="wrapper">
      …
    </div>
    
    .wrapper {
      overflow: clip;
      height: 100vh; /* Height of one of the layers of the parallax */
    }
    
  2. Объявите слои. Внутри контейнера добавьте отдельные слои, которые будут двигаться с разной скоростью.

    <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 {
      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 для резервной стратегии, так как он неполный и имеет много известных проблем.
    • Если анимация используется только в декоративных целях, выберите прогрессивное улучшение и НЕ добавляйте резервный вариант.
  • Уважайте предпочтения пользователей. Некоторые пользователи предпочитают, чтобы на веб-страницах было меньше движения. Используйте медиазапрос prefers-reduced-motion, чтобы отключить или уменьшить анимацию для этих пользователей.
  • РЕКОМЕНДУЕТСЯ анимировать только эффективные свойства CSS. Чтобы анимация была максимально плавной, анимируйте свойства, которые могут обрабатываться потоком компоновщика браузера, например transform и opacity. Анимация других свойств, таких как width или height, может привести к проблемам производительности.
  • Правильно указывайте порядок объявления. При использовании сокращенного свойства animation объявляйте animation-timeline и animation-range после него, чтобы сокращенное свойство не сбрасывало временную шкалу.

Чтобы задать animation-range:

  • РЕКОМЕНДУЕТСЯ задавать для всех слоев одинаковое смещение начала, например entry 25%.
  • РЕКОМЕНДУЕТСЯ задавать для всех слоев разные конечные смещения, используя sibling-count() и sibling-index() для распределения смещений, например exit calc(100% / sibling-count() * sibling-index()).

Поддержка браузеров и резервные стратегии

Поэтому обычно требуется резервная стратегия.

Для браузеров, которые не поддерживают анимацию на основе прокрутки, можно использовать резервный вариант, чтобы воссоздать визуальные эффекты. Резервные варианты обычно создаются с помощью прослушивателя прокрутки (для эффектов 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();
}