Реализация CSS Scroll-Driven Animations на сайте
CSS Scroll-Driven Animations — нативный браузерный API, позволяющий привязывать CSS @keyframes к позиции скролла без JavaScript. Появился в Chrome 115, Firefox 110 (частично), Safari — пока без поддержки (2025). Для производственного использования нужен полифил. Главное преимущество перед JS-решениями: анимации выполняются на compositor thread, не блокируют главный поток.
Базовая концепция
Два типа timeline:
-
scroll()— привязка к позиции скролла скролл-контейнера -
view()— привязка к видимости элемента в viewport (аналог IntersectionObserver)
/* Прогресс-бар чтения страницы */
@keyframes grow-bar {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.reading-progress {
position: fixed;
top: 0; left: 0;
width: 100%; height: 3px;
background: #3b82f6;
transform-origin: left;
animation: grow-bar linear;
animation-timeline: scroll(root); /* привязка к root scroll */
animation-fill-mode: both;
}
view(): анимации при появлении элементов
/* Появление карточек при скролле */
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: fade-up ease-out both;
animation-timeline: view();
/* Диапазон: от момента входа до 40% видимости */
animation-range: entry 0% entry 40%;
}
/* Параллакс для изображений */
@keyframes parallax-img {
from { object-position: 50% 30%; }
to { object-position: 50% 70%; }
}
.hero-image {
animation: parallax-img linear both;
animation-timeline: view();
animation-range: contain;
}
Именованные timeline через scroll-timeline
Когда нужно управлять анимацией из другого элемента:
/* Родительский контейнер создаёт именованный timeline */
.scroll-container {
overflow-y: scroll;
scroll-timeline: --my-scroll block;
height: 100vh;
}
/* Дочерний элемент использует этот timeline */
.animated-sidebar {
animation: slide-in linear both;
animation-timeline: --my-scroll;
animation-range: 0% 30%;
}
@keyframes slide-in {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
Сложный пример: sticky header трансформация
/* Хедер меняется при скролле */
@keyframes header-shrink {
from {
padding: 24px 40px;
background: transparent;
backdrop-filter: none;
}
to {
padding: 12px 40px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(12px);
}
}
.site-header {
position: sticky;
top: 0;
animation: header-shrink linear both;
animation-timeline: scroll(root);
animation-range: 0px 200px; /* первые 200px скролла */
}
Полифил для Safari
npm install @scroll-timeline-polyfill/scroll-timeline
// app/layout.tsx (Next.js) или index.html script
async function loadScrollTimelinePolyfill() {
const isSupported = CSS.supports('animation-timeline: scroll()')
if (!isSupported) {
await import('@scroll-timeline-polyfill/scroll-timeline')
}
}
loadScrollTimelinePolyfill()
Альтернатива — условный JS-фоллбэк через IntersectionObserver для критичных анимаций.
Прогрессивное улучшение: @supports
/* Базовый стиль для всех браузеров */
.animated-section {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.5s, transform 0.5s;
}
.animated-section.visible {
opacity: 1;
transform: translateY(0);
}
/* Scroll-Driven для поддерживающих браузеров */
@supports (animation-timeline: scroll()) {
.animated-section {
opacity: 1;
transform: none;
transition: none;
animation: fade-up ease-out both;
animation-timeline: view();
animation-range: entry 0% entry 50%;
}
}
JavaScript фоллбэк через IntersectionObserver:
// utils/scroll-animation-fallback.ts
export function initScrollAnimationFallback() {
// Пропускаем если нативная поддержка есть
if (CSS.supports('animation-timeline: scroll()')) return
const observer = new IntersectionObserver(
entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible')
observer.unobserve(entry.target)
}
})
},
{ rootMargin: '-10% 0px -10% 0px', threshold: 0.1 }
)
document.querySelectorAll('.animated-section').forEach(el => {
observer.observe(el)
})
}
animation-range подробнее
/* Именованные ключевые слова для view() */
/* entry: элемент входит в scroll-port */
animation-range: entry 0% entry 100%;
/* exit: элемент выходит */
animation-range: exit 0% exit 100%;
/* contain: пока элемент полностью виден */
animation-range: contain;
/* cover: от начала входа до конца выхода */
animation-range: cover 0% cover 100%;
/* Комбинированный: вход + выход */
.element {
animation: appear linear both;
animation-timeline: view();
animation-range: entry 10% exit 90%;
}
Типичные сроки
Прогресс-бар чтения + 3–4 fade-анимации при скролле — 4–6 часов. Полноценная система с полифилом, @supports, JS-фоллбэком, тестами в Safari и мобильных браузерах — 2–3 рабочих дня.







