Zurück zum Blog

GSAP in React und Next.js: useGSAP, Cleanup und ScrollTrigger

GSAP in React und Next.js: useGSAP, Cleanup und ScrollTrigger

5. Oktober 2026 — Animation

Veröffentlicht

5. Oktober 2026

Lesezeit

3 Min. Lesezeit

Kategorie

Animation

Schlagwörter

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

(Intro)So nutzen Sie GSAP in React und im App Router von Next.js ohne Speicherlecks und doppelte Animationen. useGSAP, Scoping, contextSafe, ScrollTrigger und Seitenwechsel.

GSAP ist nach wie vor die leistungsfähigste Animationsbibliothek im Web, und seit Version 3.13 ist jedes Plugin kostenlos – ScrollTrigger, SplitText, MorphSVG, alle. React rendert aber, rendert neu, unmountet und führt Effekte (in der Entwicklung) doppelt aus. Eine Animationsbibliothek, die das DOM direkt verändert, braucht daher etwas Sorgfalt, damit sie sich einfügt.

Das ist das Setup, das ich in Projekten mit dem Next.js App Router verwende, diese Website eingeschlossen.

Installation

bash
npm install gsap @gsap/react

@gsap/react liefert den Hook useGSAP, der den Teil übernimmt, den die meisten falsch machen: das Aufräumen.

Das Grundmuster

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>
  )
}

Hier passieren drei Dinge:

  1. 'use client'. GSAP greift auf das DOM zu, also muss die Komponente im Browser laufen. Halten Sie den animierten Teil klein und lassen Sie den Rest der Seite als Server Components.
  2. scope: container. Selektoren wie .hero-line greifen nur innerhalb dieser Komponente, sodass zwei Instanzen nie die Elemente der jeweils anderen animieren.
  3. Automatisches Aufräumen. Jeder Tween, jede Timeline und jeder ScrollTrigger, der im Callback entsteht, wird beim Unmount zurückgesetzt. Deshalb hinterlässt auch der doppelte Aufruf von React in der Entwicklung keine doppelten Animationen.

Bei Änderungen neu ausführen

Übergeben Sie dependencies, um bei Datenänderungen neu auszuführen, und revertOnUpdate, wenn der vorige Durchlauf zuerst rückgängig gemacht werden soll:

tsx
useGSAP(
  () => {
    gsap.to('.bar', { scaleX: progress, transformOrigin: 'left', duration: 0.4 })
  },
  { scope: container, dependencies: [progress] },
)

Event-Handler: contextSafe

Animationen, die nach der Ausführung des Hooks entstehen – etwa in einem Klick-Handler –, werden nicht automatisch fürs Aufräumen erfasst. Packen Sie diese Funktionen in 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

Registrieren Sie es einmal (ein Aufruf auf Modulebene genügt) und legen Sie Trigger innerhalb von useGSAP an:

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>
}

Pinning und Scrubbing

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 },
)

Nach Layoutänderungen aktualisieren

ScrollTrigger misst Positionen beim Anlegen. Ändern Bilder, Schriften oder spät geladene Inhalte danach die Seitenhöhe, verrutschen Start- und Endpositionen. Rufen Sie ScrollTrigger.refresh() auf, sobald alles da ist:

ts
document.fonts.ready.then(() => ScrollTrigger.refresh())

Bildern feste Abmessungen (oder ein aspect-ratio) zu geben, verhindert das meiste davon von vornherein.

Fallstricke speziell in Next.js

Seitenwechsel und Scrollposition

Im App Router werden beim Navigieren die Komponenten der alten Seite unmountet (sodass useGSAP ihre Trigger aufräumt) und die neuen gemountet. Feuern Trigger nach dem Navigieren an falschen Positionen, hat die neue Seite meist gemessen, bevor ihre Bilder geladen waren – aktualisieren Sie nach dem Laden, wie oben.

Server Components können nicht animieren

useGSAP lässt sich nicht in eine Server Component setzen. Statt eine ganze Seite zur Client Component zu machen, wickeln Sie nur den animierten Teil in einen kleinen Client-Wrapper und reichen serverseitig gerenderte Inhalte als children hinein. Der Inhalt bleibt im HTML (gut für SEO und LCP), die Bewegung kommt obendrauf.

Aufblitzen nicht animierter Inhalte vermeiden

gsap.from() läuft nach der Hydration. Für einen Frame erscheint das Element also im Endzustand und springt dann in den Startzustand. Zwei Lösungen:

  • gsap.fromTo() zusammen mit CSS, das den Startzustand setzt (opacity: 0 im Stylesheet) – und akzeptieren, dass das Element nur mit JavaScript erscheint. Nur für unkritische Elemente.
  • Besser für alles im sichtbaren Bereich: aus einem sichtbaren Zustand animieren (ein kleiner y-Versatz, keine Änderung der Deckkraft), damit schon das erste Zeichnen stimmt. Ihr LCP wird es Ihnen danken – siehe Core Web Vitals für animierte Websites.

Barrierefreiheit: matchMedia

Mit gsap.matchMedia() legen Sie Animationen nur an, wenn eine Media Query zutrifft, und setzen sie automatisch zurück, sobald sie nicht mehr zutrifft:

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 },
)

Besucher, die reduzierte Bewegung bevorzugen, bekommen den Inhalt ohne die Bewegung. Dieselbe API regelt auch Effekte nur für den Desktop ('(min-width: 768px)').

Tipps zur Performance

  • Animieren Sie x, y, scale, rotation und opacity; GSAP bildet sie auf Transforms ab.
  • Verwenden Sie gsap.quickTo() für Werte, die sich ständig ändern, etwa einen Cursor-Follower – es nutzt einen einzigen Tween wieder, statt pro Event einen neuen anzulegen.
  • Halten Sie die Zahl gleichzeitiger ScrollTrigger im Rahmen; fassen Sie ähnliche Elemente mit ScrollTrigger.batch() zusammen.

Zusammenfassung

  • useGSAP mit scope für jede animierte Komponente
  • contextSafe für Animationen aus Event-Handlern
  • Plugins einmal registrieren; ScrollTrigger innerhalb von useGSAP anlegen
  • ScrollTrigger nach Schriften und Bildern aktualisieren
  • Inhalte in Server Components lassen und über Client-Wrapper animieren
  • prefers-reduced-motion mit gsap.matchMedia() respektieren

Newsletter

Neue Beiträge per E-Mail

Eine kurze E-Mail, wenn es etwas Neues gibt. Kein Spam, jederzeit abbestellbar.

Designsysteme für kleine Teams: Mit Tokens anfangen
Designsysteme für kleine Teams: Mit Tokens anfangen
Nächster Beitrag