Реалізація 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 робочих дні.







