12k
All articles

Запуск CSS-анимаций при прокрутке без JavaScript

Создавайте CSS-анимации при прокрутке без JavaScript. Разберите синтаксис, действия триггеров, границы прокрутки и резервный вариант с IntersectionObserver.

OpenReplay Team
OpenReplay Team
Запуск CSS-анимаций при прокрутке без JavaScript

Анимации, запускаемые прокруткой (scroll-triggered animations), используют прокрутку только для того, чтобы определить момент старта обычной CSS-анимации. Когда элемент достигает заданной позиции прокрутки, анимация воспроизводится по собственным часам. Они объявляются с помощью свойств timeline-trigger и animation-trigger, и IntersectionObserver для этого не нужен.

Код для эффекта «появление при прокрутке» почти везде одинаков: observer, порог срабатывания, переключение класса и вызов unobserve(), который кто-нибудь обязательно забывает добавить. Теперь всё это можно сделать средствами CSS.

В статье разобраны синтаксис, полный пример анимации появления, три варианта поведения триггера, выбор границ прокрутки и безопасное внедрение функции, которая пока поддерживается только в Chromium.

Ключевые выводы

  • Для анимации, запускаемой прокруткой, нужны два объявления. timeline-trigger задаёт имя триггера и диапазон прокрутки, в котором он активируется. animation-trigger указывает анимации, за каким триггером следить и что делать при его срабатывании.
  • Диапазон entry 100% exit 0% активирует триггер только тогда, когда элемент целиком находится внутри области прокрутки (scrollport). В документации Chrome он считается эквивалентом ключевого слова contain.
  • play-forwards play-backwards воспроизводит анимацию при входе элемента в диапазон и проигрывает её в обратном направлении при выходе. play-once из черновика Animation Triggers запускает её только один раз.
  • Анимация, запущенная триггером, сохраняет собственные длительность и функцию плавности, поэтому при быстрой прокрутке она не ускоряется.
  • Согласно данным Chrome о поддержке, анимации, запускаемые прокруткой, работают только в браузерах на Chromium. Поэтому скрывайте контент внутри @supports и сохраняйте фолбэк на IntersectionObserver.

Чем анимации, управляемые прокруткой, отличаются от анимаций, запускаемых прокруткой?

Анимации, управляемые прокруткой (scroll-driven), привязывают прогресс анимации к позиции прокрутки. Анимации, запускаемые прокруткой (scroll-triggered), используют прокрутку лишь для того, чтобы решить, когда стартует обычная анимация. Первая модель описана в нашем руководстве о том, как создавать анимации прокрутки только на CSS: animation-timeline с scroll() и view(). Там временная шкала заменяет часы, поэтому при обратной прокрутке анимация воспроизводится в обратном направлении. Эта статья посвящена timeline-trigger и animation-trigger, которые запускают обычную анимацию с её собственной длительностью, как только элемент достигает выбранного диапазона прокрутки.

Если вам нужен индикатор прогресса чтения или параллакс, используйте CSS-анимации прогресса прокрутки. Если нужно, чтобы карточка плавно появлялась за 400 мс после того, как стала видимой, используйте триггер. В своём обзоре Web UI с Google I/O 2026 Chrome перечисляет эти две возможности как отдельные функции и описывает триггеры как способ заменить IntersectionObserver средствами CSS. Проще говоря, триггер — это CSS-версия подхода «добавить класс, когда элемент становится видимым».

Пример плавного появления при прокрутке

Для плавного появления при прокрутке нужны три составляющие: правило @keyframes, свойство animation с режимом заполнения forwards и триггер, связывающий анимацию с временной шкалой видимости (view timeline). Синтаксис определён в анонсе анимаций, запускаемых прокруткой, от Chrome. Главное в нём: триггер и анимация могут находиться на разных элементах, а источником для триггера служит временная шкала прогресса прокрутки или видимости.

<section class="reveal">
  <div class="reveal-content">
    <h2>Shipping notes</h2>
    <p>This block fades and slides in once it is fully on screen.</p>
  </div>
</section>
@keyframes reveal {
  to {
    opacity: 1;
    translate: 0 0;
  }
}

@supports (timeline-trigger-name: --t) {
  @media (prefers-reduced-motion: no-preference) {
    .reveal {
      timeline-trigger: --reveal view() entry 100% exit 0%;
      trigger-scope: --reveal;
    }

    .reveal-content {
      opacity: 0;
      translate: 0 2rem;
      animation: reveal 400ms ease-out forwards;
      animation-trigger: --reveal play-forwards;
    }
  }
}

Как это работает:

  • Триггер находится на секции, а движение — на её дочернем элементе. Сдвигаемый элемент — не тот, чья видимость отслеживается, поэтому смещение translate не может сдвинуть границу срабатывания триггера.
  • entry 100% exit 0% активирует триггер, когда секция целиком оказывается внутри области прокрутки.
  • play-forwards запускает анимацию при каждой активации триггера. Режим заполнения forwards после этого удерживает последний ключевой кадр.
  • Скрытое начальное состояние задано внутри запроса поддержки. Браузеры без поддержки функции никогда не применяют opacity: 0, поэтому контент остаётся видимым.
  • trigger-scope делает имя --reveal локальным для каждой секции. По умолчанию имя триггера видно любому элементу на странице — так же, как в случае anchor-name; это описано в примечаниях к выпуску Chrome 146.

Если указать только play-forwards, выход из диапазона ни на что не влияет: при прокрутке дальше элемент сохраняет конечное состояние. Когда он снова становится полностью видимым, триггер активируется повторно и анимация воспроизводится заново. В следующем разделе показано, как изменить это поведение.

Выбор действия триггера

animation-trigger принимает имя триггера и до двух действий. Первое выполняется при активации триггера, второе — при деактивации. От выбранного действия зависит, что происходит при повторном входе элемента в диапазон и при выходе из него.

Желаемое поведениеЗначение animation-triggerПримечание
Повтор при каждом повторном входе--reveal play-forwardsВозможен заметный скачок обратно к начальному состоянию
Обратное воспроизведение при выходе--reveal play-forwards play-backwardsПосле обратного воспроизведения элемент отображается с базовыми стилями
Однократный запуск, затем фиксация--reveal play-onceКлючевое слово из черновика, может измениться
animation-trigger: --reveal play-forwards play-backwards; /* reverses on exit */
animation-trigger: --reveal play-once; /* runs once; Animation Triggers draft keyword */

Режим заполнения forwards удерживает ключевой кадр to после завершения play-forwards. После завершения play-backwards он ничего не удерживает: анимация перестаёт применяться, и элемент возвращается к базовым стилям. В этом примере базовые стили соответствуют скрытому начальному состоянию (opacity: 0; translate: 0 2rem), поскольку в ключевых кадрах задан только to, так что элемент всё равно оказывается скрытым. Если в ваших ключевых кадрах задано состояние from, отличающееся от базовых стилей, используйте both, чтобы это состояние сохранялось после обратного воспроизведения. play-once взят из Editor’s Draft спецификации CSS Animation Triggers Level 1, а не из анонса Chrome, поэтому, прежде чем полагаться на него, проверьте его работу в целевой сборке Chrome. Это ближайший аналог вызова unobserve() после первого пересечения.

Диапазон активации и активный диапазон

В триггере прокрутки диапазон активации определяет, когда триггер срабатывает. Необязательный активный диапазон, указываемый после косой черты, определяет, как долго триггер остаётся активным, и Chrome требует, чтобы он охватывал диапазон активации. Согласно Chrome, entry 100% exit 0% эквивалентно contain, а entry 0% exit 100% — cover, поэтому следующие две строки равнозначны:

timeline-trigger: --reveal view() entry 100% exit 0% / entry 0% exit 100%;
timeline-trigger: --reveal view() contain / cover;

При такой настройке триггер включается, когда элемент полностью виден, и остаётся активным, пока видна хотя бы его часть. Это важно для play-backwards: обратная анимация начинается только после того, как элемент уйдёт за пределы экрана.

Выбор подходящих границ прокрутки

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

СитуацияДиапазонПричина
Небольшая карточка или заголовокentry 100% exit 0% (contain)Срабатывает, когда виден весь элемент
Высокая секцияentry 25% exit 0%Для высоких элементов contain может срабатывать слишком поздно
Под липкой шапкой высотой 4remview(block 4rem 0)Шапка не перекрывает элемент
/* Tall element: fire when a quarter has entered */
timeline-trigger: --reveal view() entry 25% exit 0%;

/* 4rem sticky header: start (top) inset first */
timeline-trigger: --reveal view(block 4rem 0) entry 100% exit 0%;

В view() начальный отступ указывается перед конечным. На вертикальной блочной оси начало — это верх, поэтому высота шапки, расположенной сверху, идёт первой.

Поддержка браузерами и фолбэки

В обзоре Web UI с Google I/O 2026 указано, что анимации, запускаемые прокруткой, поддерживаются в Chrome и Edge 146, а в Firefox и Safari поддержки нет. В анонсе Chrome от декабря 2025 года говорилось, что функция появится в Chrome 145, однако она вышла в Chrome 146 (стабильный выпуск 10 марта 2026 года). В посте о бета-версии Chrome 146 trigger-scope указан в том же выпуске. Рассматривайте всю функцию как прогрессивное улучшение.

Обёртка @supports в примере решает эту задачу. Браузеры без поддержки пропускают весь блок и отображают контент статически. Если вам нужна та же анимация в других браузерах, сохраните существующее переключение класса через IntersectionObserver вне запроса поддержки и запускайте его, только если CSS.supports() возвращает false. В нашей статье о замене библиотек анимации нативными веб-API рассказано, как построить такой фолбэк без зависимостей в духе AOS.

Благодаря медиазапросу prefers-reduced-motion: no-preference пользователи, предпочитающие меньше движения, видят контент сразу. Чтобы анимировать несколько элементов поочерёдно, привяжите их animation-trigger к одному общему триггеру и задайте разные задержки.

Заключение

Анимации, запускаемые прокруткой, покрывают паттерн «появление при прокрутке»: CSS определяет, когда стартует обычная анимация, а отслеживание берёт на себя браузер. Начните с приведённого выше примера. Выберите действие (повтор, обратное воспроизведение или однократный запуск), соответствующее вашему дизайну, скорректируйте диапазон для высоких элементов или липких шапок и уберите код observer для браузеров с поддержкой функции, сохранив его для остальных.

Часто задаваемые вопросы

Чем анимации, запускаемые прокруткой, отличаются от контейнерных запросов состояния прокрутки (scroll-state)?

Контейнерные запросы состояния прокрутки применяют стили, пока контейнер находится в определённом состоянии прокрутки, которым управляет браузер. Анимации, запускаемые прокруткой, запускают анимацию по ключевым кадрам, когда элемент пересекает диапазон на временной шкале видимости или прокрутки. В Chrome 133 появились состояния stuck, snapped и scrollable, а в Chrome 144 добавилось состояние scrolled, отслеживающее последнее направление прокрутки. Используйте запрос scroll-state для липкой шапки, меняющей стили после фиксации, а триггер — для эффектов появления.

Работают ли анимации, запускаемые прокруткой, быстрее, чем появление через IntersectionObserver?

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

Могут ли триггеры прокрутки управлять анимациями, созданными с помощью Web Animations API?

Да. В примечаниях к выпуску Chrome 146 сказано, что функция поставляется вместе с JavaScript-интерфейсами, поэтому триггеры могут управлять не только CSS-анимациями, но и анимациями, созданными через Web Animations API. Названия интерфейсов определены в Editor's Draft спецификации CSS Animation Triggers Level 1, который ещё может измениться, поэтому, прежде чем полагаться на них, сверьтесь с черновиком и проверьте целевую сборку Chrome.

Почему элемент остаётся скрытым после добавления триггера прокрутки?

Скрытое начальное состояние применяется сразу, как только срабатывает запрос поддержки, поэтому элемент остаётся с opacity 0, пока не активируется его триггер. Типичные причины: несовпадение написания имени в timeline-trigger и animation-trigger, анимируемый элемент вне поддерева, заданного trigger-scope, или диапазон, которого элемент никогда не достигает. Сначала проверьте имя, затем убедитесь, что анимируемый элемент находится внутри элемента, задающего область видимости.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.