GSAP je i dalje najmoćnija biblioteka za animacije na vebu, a od verzije 3.13 svaki plugin — ScrollTrigger, SplitText, MorphSVG, svi — je besplatan. Ali React renderuje, ponovo renderuje, uklanja komponente i (u razvoju) izvršava efekte dva puta, pa biblioteci koja direktno menja DOM treba malo pažnje da bi se uklopila.
Ovo je postavka koju koristim na Next.js App Router projektima, uključujući i ovaj sajt.
Instalacija
npm install gsap @gsap/react@gsap/react donosi hook useGSAP, koji rešava deo koji ljudi najčešće pogreše: čišćenje.
Osnovni obrazac
'use client'
import { useRef } from 'react'
import gsap from 'gsap'
import { useGSAP } from '@gsap/react'
gsap.registerPlugin(useGSAP)
export function Hero() {
const container = useRef<HTMLDivElement>(null)
useGSAP(
() => {
gsap.from('.hero-line', { yPercent: 100, opacity: 0, stagger: 0.08, duration: 0.9, ease: 'power3.out' })
},
{ scope: container },
)
return (
<div ref={container}>
<h1 className="hero-line">Design</h1>
<h1 className="hero-line">and build</h1>
</div>
)
}Ovde se dešavaju tri stvari:
'use client'. GSAP menja DOM, pa komponenta mora da radi u pregledaču. Neka animirani deo bude mali, a ostatak stranice neka ostane Server Components.scope: container. Selektori poput.hero-linepogađaju samo elemente unutar ove komponente, pa dve instance nikada ne animiraju elemente jedna druge.- Automatsko čišćenje. Svaki tween, timeline i ScrollTrigger napravljen u callbacku vraća se u početno stanje kada se komponenta ukloni. Zato i dvostruko pozivanje u React razvojnom režimu ne ostavlja duple animacije.
Ponovno pokretanje pri promenama
Prosledite dependencies da se callback ponovo izvrši kada se podaci promene, i revertOnUpdate ako prethodno izvršavanje treba prvo poništiti:
useGSAP(
() => {
gsap.to('.bar', { scaleX: progress, transformOrigin: 'left', duration: 0.4 })
},
{ scope: container, dependencies: [progress] },
)Handleri događaja: contextSafe
Animacije napravljene posle izvršavanja hooka — recimo u handleru klika — ne beleže se automatski za čišćenje. Obavijte te funkcije sa contextSafe:
const { contextSafe } = useGSAP({ scope: container })
const onEnter = contextSafe((event: React.MouseEvent<HTMLElement>) => {
gsap.to(event.currentTarget, { scale: 1.04, duration: 0.3 })
})
return <button onMouseEnter={onEnter}>Hover me</button>ScrollTrigger
Registrujte ga jednom (poziv na nivou modula je u redu) i pravite triggere unutar useGSAP:
'use client'
import { useRef } from 'react'
import gsap from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import { useGSAP } from '@gsap/react'
gsap.registerPlugin(useGSAP, ScrollTrigger)
export function RevealSection({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLElement>(null)
useGSAP(
() => {
gsap.utils.toArray<HTMLElement>('.reveal').forEach((el) => {
gsap.from(el, {
y: 40,
opacity: 0,
duration: 0.8,
ease: 'power2.out',
scrollTrigger: { trigger: el, start: 'top 85%' },
})
})
},
{ scope: ref },
)
return <section ref={ref}>{children}</section>
}Pinovanje i scrub
useGSAP(
() => {
gsap.timeline({
scrollTrigger: {
trigger: '.story',
start: 'top top',
end: '+=200%',
scrub: 1, // follows the scrollbar, with 1 s of smoothing
pin: true, // holds the section while the timeline plays
},
})
.to('.story-image', { scale: 1.2 })
.to('.story-text', { yPercent: -50 }, '<')
},
{ scope: ref },
)Osvežite posle promena rasporeda
ScrollTrigger meri pozicije kada se napravi. Ako slike, fontovi ili sadržaj koji kasni posle toga promene visinu stranice, početne i krajnje pozicije se pomere. Pozovite ScrollTrigger.refresh() kada se učitaju:
document.fonts.ready.then(() => ScrollTrigger.refresh())Eksplicitne dimenzije slika (ili aspect-ratio) sprečavaju većinu ovoga od početka.
Zamke specifične za Next.js
Promene ruta i pozicija skrola
U App Routeru, navigacija između stranica uklanja komponente stare stranice (pa useGSAP čisti njihove triggere) i postavlja nove. Ako posle navigacije triggeri reaguju na pogrešnim pozicijama, obično je razlog to što je nova stranica merila pre nego što su se slike učitale — osvežite posle učitavanja, kao gore.
Server Components ne mogu da animiraju
useGSAP ne možete da stavite u Server Component. Umesto da celu stranicu pretvorite u Client Component, obavijte samo animirani deo malim klijentskim omotačem i prosledite serverski renderovan sadržaj kao children. Sadržaj ostaje u HTML-u (dobro za SEO i LCP), a pokret se dodaje preko njega.
Izbegnite bljesak neanimiranog sadržaja
gsap.from() se izvršava posle hidratacije, pa se element na jedan kadar pojavi u konačnom stanju, a zatim skoči na početno. Dva rešenja:
- Koristite
gsap.fromTo()uz CSS koji postavlja početno stanje (opacity: 0u stylesheetu), i prihvatite da element zavisi od JavaScripta da bi se pojavio — samo za nekritične elemente. - Bolje za sve na prvom ekranu: animirajte iz vidljivog stanja (mali
ypomak, bez promene prozirnosti), pa je prvo iscrtavanje već ispravno. Vaš LCP će vam biti zahvalan — pogledajte Core Web Vitals za animirane sajtove.
Pristupačnost: matchMedia
gsap.matchMedia() vam omogućava da pravite animacije samo kada se media upit poklapa, i automatski ih vraća kada prestane da se poklapa:
useGSAP(
() => {
const mm = gsap.matchMedia()
mm.add('(prefers-reduced-motion: no-preference)', () => {
gsap.from('.card', { y: 60, opacity: 0, stagger: 0.1, scrollTrigger: '.cards' })
})
},
{ scope: container },
)Posetioci koji preferiraju manje pokreta dobijaju sadržaj bez kretanja. Isti API rešava i efekte samo za desktop ('(min-width: 768px)').
Saveti za performanse
- Animirajte
x,y,scale,rotationiopacity; GSAP ih preslikava u transformacije. - Koristite
gsap.quickTo()za vrednosti koje se stalno menjaju, poput elementa koji prati kursor — ponovo koristi jedan tween umesto da pravi novi za svaki događaj. - Držite broj istovremenih ScrollTriggera razumnim; slične elemente grupišite sa
ScrollTrigger.batch().
Rezime
useGSAPsascopeza svaku animiranu komponentucontextSafeza animacije napravljene u handlerima događaja- Pluginove registrujte jednom; ScrollTriggere pravite unutar
useGSAP - Osvežite ScrollTrigger posle fontova i slika
- Sadržaj držite u Server Components i animirajte ga iz klijentskih omotača
- Poštujte
prefers-reduced-motionuzgsap.matchMedia()

