Реалізація GSAP-анімацій на сайті
GSAP (GreenSock Animation Platform) — промисловий стандарт для складних веб-анімацій. На відміну від CSS-анімацій, GSAP забезпечує точний контроль над часовими шкалами, підтримує групування та послідовності, стабільно працює в Safari і коректно паузує/перемотує. Платна ліцензія потрібна лише для плагінів (ScrollTrigger, MorphSVG тощо) — базовий GSAP 3 є безкоштовним і достатнім для більшості завдань.
Встановлення та базова конфігурація
npm install gsap
# ScrollTrigger входить до основного пакету
Для Next.js і React важливо переконатись, що GSAP не виконується на сервері (SSR):
// lib/gsap.ts — централізована ініціалізація
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { ScrollToPlugin } from 'gsap/ScrollToPlugin'
if (typeof window !== 'undefined') {
gsap.registerPlugin(ScrollTrigger, ScrollToPlugin)
// Налаштування глобальних параметрів
gsap.config({
nullTargetWarn: false, // не попереджувати про null-елементи
trialWarn: false,
})
// Скидаємо ScrollTrigger при resize (важливо для мобільних)
ScrollTrigger.config({
ignoreMobileResize: true,
})
}
export { gsap, ScrollTrigger }
Hero-анімація при завантаженні сторінки
// components/HeroSection.tsx
import { useEffect, useRef } from 'react'
import { gsap } from '../lib/gsap'
export function HeroSection() {
const containerRef = useRef<HTMLDivElement>(null)
const headlineRef = useRef<HTMLHeadingElement>(null)
const sublineRef = useRef<HTMLParagraphElement>(null)
const ctaRef = useRef<HTMLButtonElement>(null)
const imageRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const ctx = gsap.context(() => {
// Часова шкала входу
const tl = gsap.timeline({
defaults: { ease: 'power3.out', duration: 0.8 },
})
tl
.from(headlineRef.current, {
y: 60,
opacity: 0,
duration: 1,
})
.from(
sublineRef.current,
{ y: 40, opacity: 0 },
'-=0.5' // перекриття -0.5с
)
.from(
ctaRef.current,
{ y: 20, opacity: 0, scale: 0.95 },
'-=0.4'
)
.from(
imageRef.current,
{
x: 80,
opacity: 0,
duration: 1.2,
ease: 'power2.out',
},
'<-0.6' // відносно попереднього старту
)
}, containerRef)
return () => ctx.revert() // очистка при розмонтуванні
}, [])
return (
<div ref={containerRef} className="hero-container">
<h1 ref={headlineRef}>Заголовок</h1>
<p ref={sublineRef}>Підзаголовок</p>
<button ref={ctaRef}>Почати</button>
<div ref={imageRef} className="hero-image" />
</div>
)
}
gsap.context() — обов'язковий паттерн для React: він скопіює всі GSAP-цілі до containerRef і коректно видалить анімації при виклику ctx.revert().
ScrollTrigger: анімації при скролі
// components/FeatureCards.tsx
import { useEffect, useRef } from 'react'
import { gsap, ScrollTrigger } from '../lib/gsap'
export function FeatureCards() {
const sectionRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const ctx = gsap.context(() => {
// Stagger-анімація карточок при вході у viewport
gsap.from('.feature-card', {
y: 80,
opacity: 0,
duration: 0.7,
stagger: 0.15,
ease: 'power2.out',
scrollTrigger: {
trigger: sectionRef.current,
start: 'top 75%', // анімація стартує коли секція на 75% від верху
end: 'bottom 20%',
toggleActions: 'play none none reverse',
// play = вперед при вході, reverse = назад при виході вгору
},
})
// Паралакс для фонового зображення
gsap.to('.section-bg', {
yPercent: -30,
ease: 'none',
scrollTrigger: {
trigger: sectionRef.current,
start: 'top bottom',
end: 'bottom top',
scrub: true, // прив'язка до позиції скролу
},
})
}, sectionRef)
return () => ctx.revert()
}, [])
return (
<div ref={sectionRef} className="features-section">
<div className="section-bg" />
{[1, 2, 3].map(i => (
<div key={i} className="feature-card">Card {i}</div>
))}
</div>
)
}
Горизонтальний скролл з ScrollTrigger
Популярний ефект — секція з горизонтальною прокруткою при вертикальному скролі:
// components/HorizontalScroll.tsx
import { useEffect, useRef } from 'react'
import { gsap, ScrollTrigger } from '../lib/gsap'
export function HorizontalScroll({ items }: { items: string[] }) {
const containerRef = useRef<HTMLDivElement>(null)
const trackRef = useRef<HTMLDivElement>(null)
useEffect(() => {
const ctx = gsap.context(() => {
const track = trackRef.current!
const totalWidth = track.scrollWidth - track.offsetWidth
gsap.to(track, {
x: -totalWidth,
ease: 'none',
scrollTrigger: {
trigger: containerRef.current,
pin: true, // фіксуємо секцію
scrub: 1, // гладкість 1с
start: 'top top',
end: `+=${totalWidth}`, // довжина скролу = ширина контенту
invalidateOnRefresh: true, // пересчіт при resize
},
})
}, containerRef)
return () => ctx.revert()
}, [])
return (
<div ref={containerRef} className="overflow-hidden">
<div ref={trackRef} className="flex gap-8 w-max py-20">
{items.map((item, i) => (
<div key={i} className="w-[400px] h-[300px] flex-shrink-0 bg-gray-100 rounded-xl flex items-center justify-center">
{item}
</div>
))}
</div>
</div>
)
}
Користувацькі функції прискорення
GSAP підтримує CustomEase для унікальних кривих прискорення:
import { CustomEase } from 'gsap/CustomEase'
gsap.registerPlugin(CustomEase)
// Створюємо з cubic-bezier або SVG path
CustomEase.create('myBounce', 'M0,0 C0.14,0 0.242,0.438 0.272,0.561 0.313,0.728 0.354,0.963 0.362,1 0.37,0.985 0.414,0.873 0.455,0.811')
gsap.to('.element', {
x: 300,
ease: 'myBounce',
duration: 1.2,
})
Очистка при навігації (Next.js App Router)
У Next.js 13+ з App Router компоненти монтуються/демонтуються при навігації. ScrollTrigger потрібно коректно видаляти:
// hooks/useGSAPScrollTrigger.ts
import { useEffect, useLayoutEffect } from 'react'
import { gsap, ScrollTrigger } from '../lib/gsap'
// useLayoutEffect для синхронного вимірювання DOM
export function useGSAPScrollTrigger(
setup: (context: gsap.Context) => void,
deps: any[] = []
) {
const isomorphicEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect
isomorphicEffect(() => {
const ctx = gsap.context(setup)
return () => {
ctx.revert()
// Явний killAll потрібен при швидкій навігації
ScrollTrigger.getAll().forEach(t => t.kill())
}
}, deps)
}
Продуктивність
Кілька правил для плавних 60fps:
- Анімувати лише
transformіopacity— вони не викликають reflow - Використовувати
will-change: transformлише для активних анімацій, видаляти після -
gsap.set()замість CSS для початкових станів — GSAP оптимізує батчинг -
ScrollTrigger.batch()для великої кількості однотипних елементів замість окремих екземплярів
// Оптимізований batch для великих списків
ScrollTrigger.batch('.list-item', {
onEnter: elements => {
gsap.from(elements, {
opacity: 0,
y: 40,
stagger: 0.1,
duration: 0.6,
})
},
start: 'top 85%',
})
Типові терміни
Hero-анімація + 2–3 ScrollTrigger-секції — 1–2 робочих дні. Повний набір анімацій для лендинга (горизонтальний скролл, pin-секції, stagger-списки, паралакс) — 4–6 робочих днів. Користувацькі анімації для складних інтерактивних сцен — окремої оцінки після технічного завдання.







