Как запускать CSS-анимацию при прокрутке страницы

В этом пошаговом руководстве будет рассмотрено создание CSS-анимации с нуля и её включение при скролле (прокрутке вверх-вниз) страницы, когда HTML-элемент находится в поле зрения — видимой части окна, с помощью Intersection Observer API.

Настройка CSS-анимации с помощью ключевых кадров

Начнем с создания CSS-анимации, которая определяется ключевыми кадрами @keyframes.

@keyframes wipe-enter {
  0% {
    transform: scale(0, .025);
  }
  50% {
    transform: scale(1, .025);
  }
}

Анимацию можно назвать как угодно, пусть будет wipe-enter. Здесь HTML-элемент будет увеличиваться в ширину, а затем в высоту.

После того, как определены ключевые кадры, можно использовать анимацию для элемента, установив свойству animation-name название ключевых кадров: wipe-enter. Кроме этого необходимо установить animation-duration — продолжительность анимации и animation-iteration-count — счетчик итераций, указывающий, сколько раз анимация должна воспроизводиться, например — бесконечно.

@media (prefers-reduced-motion: no-preference) {
  .square {
    animation-name: wipe-enter;
    animation-duration: 1s;
    animation-iteration-count: infinite;
  }
}

CSS-класс .square пусть будет заключён в медиа-запрос prefers-reduce-motion: no-preference. Это обеспечит запуск анимации только в том случае, если пользователь не ограничил её и не включил параметр «уменьшить движение» в своей операционной системе.

В этом примере будет анимирован HTML-элемент квадратной формы:

See this code CSS-animation width+height demo on x.xhtml.ru.

Для CSS-анимации можно использовать сокращённое написание свойства animation, например:

@media (prefers-reduced-motion: no-preference) {
  .square {
    animation: wipe-enter 1s infinite;
  }
}

Управление анимацией с помощью CSS-класса

Допустим, не нужно, чтобы анимация воспроизводилась сразу. Воспроизведением анимации можно управлять, добавляя HTML-элементу CSS-класс переключатель только анимации, который не используется для остальной стилизации элемента.

.square {
  width: 200px;
  height: 200px;
  background: #e62a52;
  // и т.д.
}

@media (prefers-reduced-motion: no-preference) {
  .square-animation {
    animation: wipe-enter 1s 1;
  }
}

CSS-анимация будет воспроизводиться, когда HTML-элементу будет добавлен CSS-класс .square-animation:

See this code CSS-animation with class toggler demo on x.xhtml.ru.

В этом примере CSS-анимация воспроизводится не каждый раз при нажатии на кнопку, а только тогда, когда HTML-элементу будет добавлен CSS-класс .square-animation.

Несмотря на то, что анимация должна показывать появление элемента, элемент отображается и до добавления класса .square-animation. Это сделано для того, чтобы при первой загрузке элемент был виден пользователю, даже если JavaScript заблокирован или не работает.

Добавление CSS-класса, когда элемент появляется при скролле (прокрутке страницы вверх-вниз)

В примере выше была реализована CSS-анимация, которая запускается при добавлении HTML-элементу CSS-класса. Вместо перехвата события нажатия кнопки, для добавления и удаления CSS-класса можно использовать несколько вариантов обнаружения состояния, когда элемент при скролле появляется в видимой части окна.

Вот три способа определить, когда элемент находится в видимой области окна:

  1. Использовать Intersection Observer API
  2. Измерять смещение элемента при скролле страницы
  3. Использовать стороннюю JavaScript-библиотеку, которая реализует №1 или №2

Для базовой анимации с запуском при скролле, оптимально использование Intersection Observer API, потому что он требует меньше кода, удобнее и лучше с точки зрения производительности.

API-интерфейс Intersection Observer позволяет отслеживать момент пересечения одного элемента с другим, и сообщает, когда это происходит. Этот способ идеально подходит для запуска CSS-анимации при скролле страницы. Всё, что нужно знать — когда HTML-элемент пересекается с окном просмотра. Если он пересекается, значит — находится в видимой области окна и в этот момент надо запустить CSS-анимацию.

Intersection Observer API можно рассматривать, как обычный слушатель событий, но с некоторыми дополнительными опциями. Вместо того, чтобы прикреплять прослушивание событий к HTML-элементу, надо заставить наблюдателя отслеживать элемент и его положение на странице.

Начнём с создания наблюдателя и заставим его отслеживать HTML-элемент:

// Создать наблюдателя
const observer = new IntersectionObserver(entries => {
  // Заполним метод обратного вызова позже...
});

// Сообщить наблюдателю, какие элементы следует отслеживать
observer.observe(document.querySelector('.square'));

По умолчанию корневым элементом, который будет проверяться на пересечение, является окно браузера, поэтому наблюдателю нужно только сообщить об анимируемом HTML-элементе.

Когда функция обратного вызова (callback) запускается, она возвращает массив записей из целевых (target) элементов, которые были запрошены, а также некоторую дополнительную информацию о них. В функцию всегда будет возвращаться массив, даже если наблюдение ведётся только за одним элементом, как здесь.

В функции обратного вызова можно перебрать массив записей, чтобы указать, что с ними нужно сделать. Каждая запись имеет свойство isIntersecting, которое может быть true или false. Если оно возвращает true, это означает, что элемент находится в видимой области окна (viewport).

entries.forEach(entry => {
  if (entry.isIntersecting) {
    // Элемент появился,
    // надо добавить CSS-класс для анимации
  }
});

Собираем всё вместе. Обратите внимание, что entry — это объект, предоставленный наблюдателем, а entry.target — это фактический элемент, за которым который ведется наблюдение, поэтому именно ему нужно добавить CSS-класс для запуска анимации.

const observer = new IntersectionObserver(entries => {
  // перебор записей
  entries.forEach(entry => {
    // если элемент появился
    if (entry.isIntersecting) {
      // добавить ему CSS-класс
      entry.target.classList.add('square-animation');
    }
  });
});
observer.observe(document.querySelector('.square'));

Теперь, когда HTML-элемент пересекает границы окна браузера, ему будет добавлен CSS-класс, который будет воспроизводить анимацию.

See this code Use IntersectionObserver API for add class name on x.xhtml.ru.

Если нужно, чтобы анимация запускалась каждый раз, когда HTML-элемент входит в видимую область окна, необходимо удалять CSS-класс запуска анимации, когда он находится за пределами видимой области окна.

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

<div class="square-wrapper">
  <div class="square"></div>
</div>

Теперь надо наблюдать за HTML-элементом c CSS-классом square-wrapper а класс для анимации применять к элементу с классом square, как и прежде. Когда элемент-оболочка находится за пределами видимой области, нужно удалять CSS-класс у элемента .square, чтобы анимация перезапускалась каждый раз, когда элемент появляется в окне при скролле.

const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    const square = entry.target.querySelector('.square');
    if (entry.isIntersecting) {
      square.classList.add('square-animation');
	  return; // если класс добавлен, продолжать уже не надо
    }
    // перемещение завершено, теперь надо удалить класс
    square.classList.remove('square-animation');
  });
});
observer.observe(document.querySelector('.square-wrapper'));

Чтобы элемент-оболочку было видно, для примера, ему добавлена пунктирная рамка. Попробуйте прокрутить вверх и вниз документ в окне ниже:

See this code Use IntersectionObserver API for add-remove class name on x.xhtml.ru.

Теперь — порядок! Добавляя и удаляя CSS-класс каждый раз, когда при скролле страницы HTML-элемент входит в область просмотра, запускается CSS-анимация.

Запуск CSS-transition при скролле

Если для анимации используется только один шаг и не требуется применение @keyframes, например, достаточно изменения прозрачности HTML-элемента от 0 до 1, можно использовать CSS-transition вместо CSS-animation.

Метод по сути тот же. Вместо определения ключевых кадров (@keyframes) для CSS-класса анимации указаны свойства для transition.

.square {
  width: 200px;
  height: 200px;
  background: #e62a52;
  border-radius: 8px;
  opacity: 0;
  transform: scale(1.2);
}
@media (prefers-reduced-motion: no-preference) {
  .square {
    transition: opacity 1.5s ease, transform 1.5s ease;
  }
}
.square-transition {
  opacity: 1;
  transform: none;
}

Заставим HTML-элемент появляться, когда он перемещается в видимую область просмотра. Для этого в момент появления в окне при скролле ему надо добавить CSS-класс square-transition. Так пользователь, у которого заблокирован или не загружается JavaScript, должен всё-равно увидеть HTML-элемент в его окончательном состоянии.

<div class="square-wrapper">
  <div class="square square-transition"></div>
</div>

Это особенно важно, поскольку анимация начинается с opacity: 0. Если бы не было настройки CSS-класса square-transition и JavaScript не работал, пользователь вообще не увидел бы HTML-элемент! Но если эффект перехода заключается в том, чтобы что-то исчезло, наверное, это не понадобится делать.

При первом запуске JavaScript нужно удалить CSS-класс, чтобы затем, его можно было добавить обратно, когда действительно нужно запустить transition. Это должно быть сделано вне метода обработки наблюдения за скроллом, желательно в начале JavaScript. Вот полный код:

// Удалить CSS-класс square-transition
const square = document.querySelector('.square');
square.classList.remove('square-transition');

// Добавить наблюдение за появлением элемента
const observer = new IntersectionObserver(entries => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      square.classList.add('square-transition');
      return;
    }
    square.classList.remove('square-transition');
  });
});
observer.observe(document.querySelector('.square-wrapper'));

В этом примере CSS-transition запускается, когда элемент-оболочка при скролле появляется в окне. Если Javascript не запустился, элемент всё-равно будет виден.

See this code Use IntersectionObserver API for css-transition on x.xhtml.ru.

Как видите, эту технику можно расширять разными способами, чтобы создать множество эффектов анимации.