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-заголовки с изменением стилей

Предпосылки

Два типа 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);
  }
}

Частые ошибки

  1. Забыли animation-timeline — анимация будет работать по времени, а не по скроллу
  2. animation-duration со scroll-timeline — длительность игнорируется (управляется скроллом), но ставьте linear для easing
  3. Нет скролла = нет анимации — если контент не прокручивается, scroll-timeline не работает
  4. view на невидимом элементе — элемент должен быть в потоке документа

Практика

  • Создать progress bar чтения через scroll(root)
  • Сделать fade-in карточек через view + animation-range: entry
  • Реализовать параллакс фона
  • Сделать sticky header с анимированным уменьшением
  • Протестировать в Chrome DevTools (Animations panel)

Связанные темы

Ресурсы