Scroll-driven animations
Scroll-driven animations привязывают CSS-анимации к прогрессу скролла, а не ко времени.
scroll-timelineследит за прокруткой контейнера,view-timeline— за видимостью элемента в viewport.
Зачем нужно
Раньше для анимаций при скролле нужен был JavaScript (Intersection Observer, scroll events). Scroll-driven animations позволяют делать это чистым CSS: progress bar при чтении, параллакс, fade-in при появлении, анимация по мере скролла — без единой строки JS.
Где используется
- Индикатор прогресса чтения статьи
- Fade-in / slide-in элементов при скролле
- Параллакс-эффекты
- Горизонтальные карусели с анимацией
- Sticky-заголовки с изменением стилей
Предпосылки
- CSS Animations (
@keyframes,animation) - Единицы измерения — viewport-единицы
Два типа scroll timelines
1. Scroll Progress Timeline (scroll)
Привязка к прогрессу прокрутки контейнера (0% = начало, 100% = конец):
/* Индикатор прогресса чтения */
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: #007bff;
transform-origin: left;
/* Анимация привязана к скроллу */
animation: grow-width linear;
animation-timeline: scroll;
}
@keyframes grow-width {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
2. View Progress Timeline (view)
Привязка к видимости элемента в viewport (0% = элемент вошёл, 100% = элемент вышел):
/* Fade-in при появлении */
.fade-in {
animation: appear linear both;
animation-timeline: view;
animation-range: entry 0% entry 100%;
}
@keyframes appear {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
animation-timeline
.element {
/* scroll — привязка к скроллу контейнера */
animation-timeline: scroll;
/* scroll с параметрами */
animation-timeline: scroll(root); /* корневой скролл */
animation-timeline: scroll(nearest); /* ближайший скроллируемый предок */
animation-timeline: scroll(self); /* собственный скролл */
animation-timeline: scroll(root block); /* вертикальный скролл */
animation-timeline: scroll(root inline); /* горизонтальный скролл */
/* view — привязка к видимости */
animation-timeline: view;
animation-timeline: view(block); /* видимость по вертикали */
animation-timeline: view(inline); /* видимость по горизонтали */
}
animation-range
Определяет, какая часть scroll/view timeline соответствует анимации:
.element {
animation: fade-in linear both;
animation-timeline: view;
/* entry — момент входа в viewport */
animation-range: entry;
/* entry 0% до entry 100% — полный вход */
animation-range: entry 0% entry 100%;
/* cover — от начала входа до полного выхода */
animation-range: cover;
/* contain — от полного входа до начала выхода */
animation-range: contain;
/* exit — момент выхода */
animation-range: exit;
/* Конкретные проценты */
animation-range: entry 25% cover 50%;
}
Визуализация animation-range для view
┌─────────────────┐
│ Viewport │
entry 0% ──→ ├─────────────────┤
│ ┌───────────┐ │
entry 100% ──→ │ │ Element │ │ ← contain 0%
│ │ │ │
contain 100% ──→ │ │ │ │
│ └───────────┘ │
exit 0% ──→ ├─────────────────┤
│ │
exit 100% ──→ └─────────────────┘
Именованные timelines
scroll-timeline
.scroller {
overflow-y: auto;
scroll-timeline-name: --my-scroller;
scroll-timeline-axis: block;
/* Сокращение */
scroll-timeline: --my-scroller block;
}
.animated-child {
animation: slide-in linear both;
animation-timeline: --my-scroller;
}
view-timeline
.tracked-element {
view-timeline-name: --card-view;
view-timeline-axis: block;
}
.related-element {
animation: highlight linear both;
animation-timeline: --card-view;
animation-range: contain;
}
Практические примеры
Индикатор прогресса чтения
.reading-progress {
position: fixed;
inset-block-start: 0;
inset-inline: 0;
block-size: 3px;
background: linear-gradient(to right, #007bff, #00d4ff);
transform-origin: left;
z-index: 1000;
animation: scale-x linear;
animation-timeline: scroll(root);
}
@keyframes scale-x {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
Появление карточек при скролле
.card {
animation: slide-up linear both;
animation-timeline: view;
animation-range: entry 10% entry 90%;
}
@keyframes slide-up {
from {
opacity: 0;
transform: translateY(60px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
Параллакс фона
.hero {
position: relative;
min-block-size: 100dvh;
overflow: hidden;
}
.hero-bg {
position: absolute;
inset: -20% 0;
background: url("bg.jpg") center/cover;
animation: parallax linear;
animation-timeline: scroll;
}
@keyframes parallax {
from { transform: translateY(-10%); }
to { transform: translateY(10%); }
}
Sticky header с изменением стиля
.header {
position: sticky;
inset-block-start: 0;
z-index: 100;
animation: shrink-header linear both;
animation-timeline: scroll;
animation-range: 0px 200px;
}
@keyframes shrink-header {
from {
padding-block: 24px;
background: transparent;
}
to {
padding-block: 8px;
background: rgba(255, 255, 255, 0.95);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
}
Частые ошибки
- Забыли
animation-timeline— анимация будет работать по времени, а не по скроллу animation-durationсо scroll-timeline — длительность игнорируется (управляется скроллом), но ставьтеlinearдля easing- Нет скролла = нет анимации — если контент не прокручивается, scroll-timeline не работает
viewна невидимом элементе — элемент должен быть в потоке документа
Практика
- Создать progress bar чтения через
scroll(root) - Сделать fade-in карточек через
view+animation-range: entry - Реализовать параллакс фона
- Сделать sticky header с анимированным уменьшением
- Протестировать в Chrome DevTools (Animations panel)
Связанные темы
- scroll-behavior — плавная прокрутка
- scroll-snap — привязка к точкам скролла