ДокументацияCSSScroll-driven animations: CSS-анимации при прокрутке
Средний 8 мин чтения

Scroll-driven animations: CSS-анимации при прокрутке

Scroll-driven animations в CSS — animation-timeline: scroll() и view(), анимации привязанные к прокрутке, progress bar, reveal-эффекты без JavaScript.

scroll-driven animationsанимации при прокруткеscroll animationsanimation-timelineCSS анимацииparallax

Что такое scroll-driven animations

Обычные CSS-анимации привязаны ко времени. Scroll-driven — к позиции прокрутки. Прокрутите вниз — анимация продвигается. Прокрутите вверх — откатывается. Никакого JavaScript и IntersectionObserver.

animation-timeline: scroll()

Привязывает анимацию к прокрутке ближайшего предка с overflow: scroll/auto:

.progress-bar {
  animation: grow linear;
  animation-timeline: scroll();
}

@keyframes grow {
  from { width: 0; }
  to { width: 100%; }
}

Полоса заполняется по мере прокрутки страницы.

Указание контейнера прокрутки

По умолчанию берётся ближайший scroll-контейнер. Можно указать конкретный через имя:

.scroll-container {
  overflow-y: auto;
  scroll-timeline-name: --my-scroll;
}

.animated-element {
  animation: fade-in linear;
  animation-timeline: scroll(--my-scroll);
}

Ось прокрутки

.animated {
  animation-timeline: scroll();
  animation-axis: block;  /* вертикальная (по умолчанию) */
  animation-axis: inline; /* горизонтальная */
}

Короткая запись:

animation-timeline: scroll(block);
animation-timeline: scroll(inline inline);
animation-timeline: scroll(--name block);

animation-timeline: view()

Анимация привязана к появлению элемента в видимой области. Начинается, когда элемент входит во viewport, заканчивается — когда выходит:

.reveal {
  animation: reveal linear both;
  animation-timeline: view();
}

@keyframes reveal {
  from {
    opacity: 0;
    transform: translateY(50px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

animation-range

Задаёт, на каком отрезке видимости работает анимация:

.reveal {
  animation-timeline: view();
  animation-range: entry 0% entry 100%;
}

Значения range:

ЗначениеКогда
entryэлемент входит в viewport
exitэлемент выходит из viewport
entry-crossingэлемент полностью вошёл
exit-crossingэлемент полностью вышел
containэлемент полностью виден

Комбинирование:

.fade-in {
  animation: fade-in linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}

@keyframes fade-in {
  from { opacity: 0; transform: translateY(30px); }
  to { opacity: 1; transform: translateY(0); }
}

Элемент появляется в первые 40% прокрутки через его область.

Практические примеры

Прогресс-бар чтения статьи

.reading-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  background: #6366f1;
  animation: progress linear;
  animation-timeline: scroll();
  z-index: 100;
}

@keyframes progress {
  from { width: 0; }
  to { width: 100%; }
}

Parallax-слои

.parallax-bg {
  animation: parallax linear;
  animation-timeline: scroll();
}

@keyframes parallax {
  from { transform: translateY(0); }
  to { transform: translateY(-150px); }
}

Разная скорость для разных слоёв — регулируйте translateY:

.slow { animation: parallax-slow linear; animation-timeline: scroll(); }
.fast { animation: parallax-fast linear; animation-timeline: scroll(); }

@keyframes parallax-slow { to { transform: translateY(-50px); } }
@keyframes parallax-fast { to { transform: translateY(-200px); } }

Reveal при появлении

.reveal-up {
  animation: reveal-up linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 30%;
}

@keyframes reveal-up {
  from {
    opacity: 0;
    transform: translateY(40px);
  }
}

.reveal-left {
  animation: reveal-left linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 40%;
}

@keyframes reveal-left {
  from {
    opacity: 0;
    transform: translateX(-40px);
  }
}

Масштабирование при скролле

.zoom-in {
  animation: zoom linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 50%;
}

@keyframes zoom {
  from {
    opacity: 0;
    transform: scale(0.8);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

Горизонтальная прокрутка (scroll snap + timeline)

.horizontal-gallery {
  display: flex;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}

.gallery-item {
  scroll-snap-align: start;
  flex-shrink: 0;
  width: 300px;
  animation: scale-in linear both;
  animation-timeline: view();
  animation-axis: inline;
}

@keyframes scale-in {
  from { transform: scale(0.9); opacity: 0.5; }
  to { transform: scale(1); opacity: 1; }
}

Фиксированная секция с меняющимся контентом

.sticky-section {
  position: sticky;
  top: 0;
  height: 100vh;
}

.sticky-section .frame {
  animation: frame-in linear both;
  animation-timeline: view();
}

@keyframes frame-in {
  0% { opacity: 0; }
  10% { opacity: 1; }
  90% { opacity: 1; }
  100% { opacity: 0; }
}

Timeline scope

Чтобы элемент анимировался по прокрутке не своего контейнера, используйте timeline-scope:

.wrapper {
  timeline-scope: --page-scroll;
  scroll-timeline-name: --page-scroll;
}

.animated-child {
  animation: grow linear;
  animation-timeline: scroll(--page-scroll);
}

Это связывает анимацию дочернего элемента с прокруткой wrapper, даже если дочерний элемент не является прямым потомком scroll-контейнера.

Поддержка

  • Chrome 115+, Edge 115+ — полная поддержка
  • Firefox — в разработке (за флагом)
  • Safari — пока нет

Фоллбэк — обычная анимация или IntersectionObserver:

.reveal {
  opacity: 0;
  transform: translateY(30px);
  animation: reveal-up linear both;
  animation-timeline: view();
}

@supports not (animation-timeline: view()) {
  .reveal {
    opacity: 1;
    transform: none;
  }
}
if (!CSS.supports('animation-timeline', 'view()')) {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible')
        observer.unobserve(entry.target)
      }
    })
  })
  document.querySelectorAll('.reveal').forEach((el) => observer.observe(el))
}

Итог

  • animation-timeline: scroll() — анимация привязана к прокрутке контейнера
  • animation-timeline: view() — анимация привязана к появлению элемента
  • animation-range — настройка начала и конца анимации
  • Не нужен JavaScript для базовых эффектов (reveal, parallax, progress)
  • Поддержка — Chrome/Edge 115+, для остальных — фоллбэк через IntersectionObserver