На главную

CSS-эффекты и графика

Анимация по прокрутке

Анимация, привязанная к прокрутке, без единой строчки JS: animation-timeline с view() и scroll(). Появление блоков, наезд на картинку, параллакс, полоса прочитанного и сжатие шапки — с запасным путём для браузеров без поддержки.

Эффект

Класс, идентификатор или тег

Как это выглядит

Прокрутите внутри рамки.

Прокрутите вниз ↓

Блок 1
Блок 2
Блок 3
Блок 4

Конец

animation-timeline работает в Chrome и Edge с 115, в Safari с 26. В Firefox — за флагом, поэтому запасной путь не украшение, а условие: без него блок так и останется прозрачным.

CSS

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

/* Блок всплывает и проявляется, пока въезжает снизу. */
.reveal {
  animation: reveal linear both;
  animation-timeline: view();
  animation-range: entry 0% cover 60%;
}

/* Анимация по прокрутке — это движение по воле пользователя, но для тех,
   кто просил меньше движения, оставляем конечное состояние без перехода. */
@media (prefers-reduced-motion: reduce) {
  .reveal { animation: none; opacity: 1; transform: none; }
}

Запасной путь

Там, где таймлайна нет, блок с нулевой прозрачностью так и останется невидимым. Эти два куска включаются только при отсутствии поддержки и ничего не делают там, где она есть.

fallback.css

@supports not (animation-timeline: view()) {
  .reveal {
    opacity: 0;
    transform: translateY(32px);
    transition: opacity 0.5s ease, transform 0.5s ease;
  }
  .reveal.is-visible {
    opacity: 1;
    transform: none;
  }
}

fallback.js

// Запасной путь для браузеров без animation-timeline: класс вешается,
// когда блок показался. Проверка идёт по поддержке, а не по названию браузера.
if (!CSS.supports('animation-timeline: view()')) {
  const targets = document.querySelectorAll('.reveal');
  const observer = new IntersectionObserver((entries) => {
    for (const entry of entries) {
      if (!entry.isIntersecting) continue;
      entry.target.classList.add('is-visible');
      observer.unobserve(entry.target);
    }
  }, { rootMargin: '0px 0px -10% 0px', threshold: 0.1 });
  targets.forEach((el) => observer.observe(el));
}