Nazad na blog

GSAP u React-u i Next.js-u: useGSAP, čišćenje i ScrollTrigger

GSAP u React-u i Next.js-u: useGSAP, čišćenje i ScrollTrigger

5. oktobar 2026. — Animacija

Objavljeno

5. oktobar 2026.

Vreme čitanja

3 min čitanja

Kategorija

Animacija

Oznake

  • GSAP
  • React
  • Next.js
  • ScrollTrigger
  • Animation

(Uvod)Kako koristiti GSAP u React-u i Next.js App Routeru bez curenja memorije i duplih animacija. useGSAP, opseg, contextSafe, ScrollTrigger i promene ruta.

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

bash
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

components/hero.tsx
'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:

  1. '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.
  2. scope: container. Selektori poput .hero-line pogađaju samo elemente unutar ove komponente, pa dve instance nikada ne animiraju elemente jedna druge.
  3. 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:

tsx
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:

tsx
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:

components/reveal-section.tsx
'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

tsx
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:

ts
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: 0 u 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 y pomak, 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:

tsx
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, rotation i opacity; 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

  • useGSAP sa scope za svaku animiranu komponentu
  • contextSafe za 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-motion uz gsap.matchMedia()

Bilten

Novi tekstovi na email

Kratak email kad objavim nešto novo. Bez spama, a pretplatu možete otkazati u bilo kom trenutku.

Dizajn sistem za male timove: počnite od tokena
Dizajn sistem za male timove: počnite od tokena
Sledeći tekst →