Scroll-driven Animations: Анимация, привязанная к прокрутке
Анимации обычно управляются временем: они запускаются, длятся заданное количество миллисекунд и заканчиваются. Но что, если нужно, чтобы прогресс анимации зависел не от времени, а от положения полосы прокрутки? Именно эту задачу решает спецификация Scroll-driven Animations.
Scroll-driven Animations позволяют привязать анимацию к прокрутке страницы или любого другого контейнера. Вместо animation-duration мы используем расстояние прокрутки: чем дальше прокручен контейнер, тем ближе анимация к своему завершению. Грубо говоря, если блок был прокручен на 50%, то анимация будет завершена на 50%.
Это открывает огромные возможности для создания интерактивных историй, параллакс‑эффектов, индикаторов чтения, анимаций появления при скролле и многого другого — всё это без единой строки JavaScript, только на CSS.
Два типа временных шкал
Спецификация определяет два вида шкал, которые можно использовать для управления анимацией:
-
Scroll Progress Timeline — привязана к прогрессу прокрутки самого контейнера. 0% — это начальное положение скролла, 100% — конечное.
-
View Progress Timeline — привязана к моменту, когда определённый элемент появляется в зоне видимости (вьюпорте) своего ближайшего скроллируемого предка. 0% — момент, когда элемент только начинает показываться, 100% — когда он полностью скрывается при дальнейшей прокрутке. Пока может звучать сложно, но на практике всё просто. Дальше разберёмся на примерах.
Анимации, использующие эти шкалы, называются scroll‑driven animations (анимации, управляемые прокруткой).
Способы объявления: анонимные и именованные шкалы
Связать анимацию со шкалой можно двумя способами:
- Анонимно, прямо в свойстве
animation-timelineс помощью функцийscroll()илиview(). - Именованно, сначала объявив шкалу у какого‑то элемента (с помощью свойств
scroll-timeline-*илиview-timeline-*), а затем сославшись на имя вanimation-timelineу любого элемента в нужной области видимости.
Рассмотрим оба подхода на примерах.
Scroll Progress Timeline — анимация, следующая за скроллом
Представим горизонтальную линию, которая заполняется по мере прокрутки страницы. Это классический индикатор чтения.
<div class="scroll-container"> <!-- scroll container -->
<div class="progress-bar"></div> <!-- progress bar -->
<div class="content"> <!-- content -->
<!-- много контента -->
<p>Lorem ipsum ...</p>
</div>
</div>
/* Контейнер с горизонтальной прокруткой */
.scroll-container {
overflow-x: auto;
}
.progress-bar {
/* Фиксированная полоса прогресса над контентом */
position: fixed;
top: 0;
left: 0;
height: 10px;
background-color: orange;
transform-origin: left;
/* Анимация заполнения полосы прогресса */
animation: scaleProgress auto linear;
/* привязка анимации к прогрессу прокрутки скролл-контейнера */
animation-timeline: scroll();
}
/* Заполнение полосы прогресса от 0 до 100% по ширине */
@keyframes scaleProgress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
Обратите внимание, что 50% прокрутки это не середина контента, а середина полосы прогресса.
Прокрутка этого блока напрямую управляет анимацией полосы прогресса сверху.
Высота контента искусственно увеличена, чтобы было легче увидеть, как шкала заполняется от 0 до 100%.
Объяснение:
animation: scaleProgress auto linear;— длительностьautoуказывает, что анимация должна занимать весь доступный диапазон шкалы.animation-timeline: scroll();— используем анонимную шкалу прогресса прокрутки ближайшего скролл‑контейнера (в данном случае ближайшего родительского блока со скролом).- По мере прокрутки страницы от самого верха до самого низа, значение
scaleXменяется от 0 до 1, где 0 это крайний левый край блока, а 1 это крайний правый край блока.
Параметры функции scroll()
Синтаксис scroll():
scroll([<scroller> || <axis>]?)
У scroll( ) два необязательных аргумента: <scroller> (какой контейнер использовать) и <axis> (какую ось отслеживать).
<scroller>:nearest(по умолчанию) — ближайший скролл‑контейнер;root— область просмотра документа;self— сам элемент, если он является скролл‑контейнером.
<axis>:block(по умолчанию, направление блока) — ось, соответствующая направлению блока (зависит от writing mode);inline— строчная ось;x— горизонтальная ось;y— вертикальная ось;
Пример использования горизонтальной прокрутки:
.container {
overflow-x: auto;
animation-timeline: scroll(self x);
}
Именованные Scroll Progress Timeline
Если нужно, чтобы несколько анимаций ссылались на одну и ту же шкалу, удобно дать ей имя с помощью свойства scroll-timeline-name (и опционально scroll-timeline-axis для оси).
/* Контейнер с вертикальной прокруткой */
.scroller {
overflow-y: auto;
/* Имя шкалы */
scroll-timeline-name: --my-scroll;
/* Ось прокрутки */
scroll-timeline-axis: y;
}
/* Три элемента, которые будут использовать одну и ту же шкалу */
/* Анимация заголовка */
.heading {
animation: animationName auto linear;
animation-timeline: --my-scroll;
}
/* Анимация полосы прогресса */
.progress-bar {
animation: animationAnotherName auto linear;
animation-timeline: --my-scroll;
}
/* Анимация карточки */
.card {
animation: animationAnotherAnotherName auto linear;
animation-timeline: --my-scroll;
}
--my-scroll. При скроле увеличивается ширина полосы прогресса, размер текста в заголовке и анимации внутри карточек.
Named Scroll Timeline
Одна шкала, три разные анимации
Квадрат поворачивается по мере прокрутки, но использует ту же timeline, что и остальные элементы.
Во второй панели капсула растёт по ширине. Источник прогресса не меняется, меняется только keyframes.
В третьей панели фигура сужается, хотя всё ещё привязана к той же самой именованной scroll timeline.
Имя шкалы должно начинаться с двух дефисов (--), чтобы не конфликтовать со стандартными ключевыми словами CSS.
Область видимости именованной шкалы — это элемент, на котором она объявлена, и все его потомки. С помощью свойства timeline-scope можно расширить область видимости на предков или другие элементы.
💡 Я рекомендую использовать именованные шкалы, так как это повышает читаемость кода и позволяет легко определить с какой шкалой связана анимация. Если анимация происходит на главной шкале сайта, то использовать
animation-timeline: scroll(root y);- также точно указывая с какой шкалой связана анимация.
Подведём промежуточный итог: у сайта или элемента может быть прокрутка, у прокрутки есть шкала на которую можно привязать анимацию. Шкалу по умолчанию анонимная, но её можно именовать. К любой шкале можно привязать сколько угодно анимаций вне зависимости от того, именованная она или анонимная. Приоритетнее всего использовать именованные шкалы, так как это повышает читаемость кода и позволяет легко определить с какой шкалой связана анимация.
View Progress Timeline — анимация появления элемента при скролле
Этот тип шкалы часто используется для анимации элементов, когда они входят во вьюпорт или выходят из него. Давайте рассмотрим пример анимации появления карточки при прокрутке.
<div class="view-demo__scroller"> <!-- scroll container -->
<div class="view-demo__card">Карточка 1</div>
<div class="view-demo__card">Карточка 2</div>
<div class="view-demo__card">Карточка 3</div>
</div>
.view-demo__scroller {
overflow-y: auto;
}
.view-demo__card {
animation-timeline: view();
/* у карточки по умолчанию белая обводка */
border: 3px dashed rgb(from **white** r g b / 1);
}
@keyframes appear {
/* от непрозрачной **синей** обводки */
from {
border-color: rgb(from blue r g b / 1);
}
/* до прозрачной **синей** обводки */
to {
border-color: rgb(from blue r g b / 0);
}
}
view(), ту которую видит пользователь. Полупрозрачные зоны сверху и снизу показывают, что карточки продолжают двигаться дальше, но уже вне этой области.
Полупрозрачные зоны сверху и снизу специально показывают участок контейнера, который исключён из активной области. Сделаны они исключительно для наглядности, чтобы было понятно, что карточки продолжают движение под этими слоями, но прогресс view() в них уже другой.
Шкала начинается, когда элемент пересекает границу активную границу view() этой области, и заканчивается, когда он полностью выходит за противоположную границу.
Алгоритм работы шкалы:
- Элемент находится вне активной границы
view(). Цвет обводки белый, по умолчанию указанный у.view-demo__card. - Элемент пересекает границу активной границу
view()этой области. Цвет обводки меняется на цветной. Из-за анимацииviewProgressAppear.@keyframes viewProgressAppear { from { border-color: rgb(from var(--accent) r g b / 1); } } - Элемент находится внутри активной границы
view(). Цвет обводки постепенно становится прозрачным по мере прокрутки. - Элемент почти полностью выходит за противоположную границу. Цвет обводки становится прозрачным из-за анимации
viewProgressAppear.@keyframes viewProgressAppear { to { border-color: rgb(from var(--accent) r g b / 0); } } - Шкала для элемента заканчивается. Элемент снова становится с белой обводкой.
Этот алгоритм работает для каждого элемента в контейнере по очереди.
Уточнение диапазона: inset и ось
Синтаксис view():
view([<axis> || <view-timeline-inset>]?)
если с axis уже всё понятно - это ось, по которой будет считаться прогресс, то с view-timeline-inset немного сложнее. view-timeline-inset позволяют сдвигать область, считающуюся «видимой».
.card {
animation-timeline: view(block 10%);
}
10% от верхней и нижней границы, с которых визуально показывается старт анимации.
Линии 10% показывают дополнительный сдвиг границ для запуска анимации.
Алгоритм работы шкалы здесь не меняется относительно предыдущего примера — меняются только точки старта и конца анимации (они сдвинуты внутрь области view()).
Алгоритм работы шкалы:
- Элемент находится вне активной границы
view(). Цвет обводки белый, по умолчанию указанный у.view-inset-demo__card. - Элемент пересекает стартовую границу активной области. Ничего не меняется
- Элемент пересекает смещённую стартовую границу активной области 10%. Цвет обводки становится синим из-за
viewInsetAppear.@keyframes viewInsetAppear { from { border-color: rgb(from var(--accent) r g b / 1); } } - Элемент находится внутри активной области. Цвет обводки постепенно становится прозрачным по мере прокрутки. Из-за того, что активная зона сузилась из-за смещения по 10% сверху и снизу - прозрачность элемента становится быстрее.
- Элемент пересекает смещённую противоположную границу активной области 10%. Цвет обводки становится прозрачным из-за
viewInsetAppear.@keyframes viewInsetAppear { to { border-color: rgb(from var(--accent) r g b / 0); } } - Элемент полностью пересёк противоположную границу 10% активной области. Цвет обводки становится белым.
- Шкала для элемента заканчивается. Элемент продолжает отображаться с белой обводкой.
Итог: поведение анимации то же самое, просто inset отодвигает момент её начала и завершения.
Именованные View Progress Timeline
Аналогично “скролловым” шкалам, можно объявить именованную шкалу с помощью свойств view-timeline-name.
.wrapper {
view-timeline-name: --card-view;
}
.element {
animation-timeline: --card-view;
}
Полной записью View Progress Timeline является:
.wrapper {
view-timeline-name: --card-view;
view-timeline-axis: block;
view-timeline-inset: 10%;
}
.element {
animation-timeline: --card-view;
}
💡 Я всё ещё рекомендую отдавать приоритет именованным шкалам.
Управление диапазоном: animation-range
Иногда нужно, чтобы анимация происходила не на всём протяжении шкалы, а только в её определённой части. Например, элемент появляется только в первой половине своего вхождения во вьюпорт, а затем уже не меняется. Для этого существует свойство animation-range (а также его составляющие animation-range-start и animation-range-end).
Именованные диапазоны View Progress Timeline
Для View Progress Timeline определены стандартные именованные отрезки:
cover— полный диапазон от первого появления элемента до полного исчезновения.
contain— диапазон, когда элемент полностью находится внутри вьюпорта.
entry— диапазон входа элемента (от 0%coverдо 0%contain).exit— диапазон выхода (от 100%containдо 100%cover).entry-crossing— когда элемент пересекает начальную (или конечную) границу вьюпорта.exit-crossing— аналогично при выходе.
Эти диапазоны можно указывать в animation-range и в ключевых кадрах @keyframes.
.card {
animation: fade auto linear;
animation-timeline: view();
animation-range: entry 0% entry 50%;
}
@keyframes fade {
from { opacity: 0; }
to { opacity: 1; }
}
Здесь анимация будет проигрываться только на первой половине диапазона entry.
Использование в ключевых кадрах
Ключевые кадры тоже можно привязывать к именованным диапазонам:
@keyframes slide {
entry 0% { opacity: 0; transform: translateX(-100%); }
entry 100% { opacity: 1; transform: translateX(0); }
exit 0% { opacity: 1; transform: translateX(0); }
exit 100% { opacity: 0; transform: translateX(100%); }
}
Теперь анимация будет менять свойства по‑разному при входе и выходе элемента.
Поддержка браузерами и будущее
На момент написания (начало 2026 года) Scroll-driven Animations поддерживаются в современных браузерах: Chrome, Edge, Opera, Safari (с 2024 года), Firefox (с 2025 года).