Запуск CSS-анимаций при прокрутке без JavaScript
Создавайте CSS-анимации при прокрутке без JavaScript. Разберите синтаксис, действия триггеров, границы прокрутки и резервный вариант с IntersectionObserver.
Анимации, запускаемые прокруткой (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 может срабатывать слишком поздно |
| Под липкой шапкой высотой 4rem | view(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, или диапазон, которого элемент никогда не достигает. Сначала проверьте имя, затем убедитесь, что анимируемый элемент находится внутри элемента, задающего область видимости.
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