Реализация 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>
)
}
Кастомные ease-функции
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 рабочих дней. Кастомные анимации для сложных интерактивных сцен — отдельная оценка после технического задания.







