Scroll-triggered animacje można odseparować per komponent i odpalać raz — mają minimalny wpływ na performance. Scroll-linked wymagają myślenia „silnikowo":
Jeden globalny driver odczytu scrolla (ScrollTrigger ticker, requestAnimationFrame)
Reużywane instancje animacji (gsap.quickTo)
Żadnego gsap.to() ani setState wewnątrz handlera scrolla
Scroll-triggered vs linked — wzorzec
1constobserver=newIntersectionObserver(
2([entry])=>{
3if(entry.isIntersecting){
4entry.target.classList.add('visible');
5observer.unobserve(entry.target);// once
6}
7},
8{threshold:0.5}
9);
10
11document.querySelectorAll('.animate-on-scroll')
12.forEach(el=>observer.observe(el));
02
UX i dostępność
Animacje powinny być subtelnym wzmocnieniem UX, nie przeszkodą. Trzy kluczowe zasady:
⚖️
Nie przesadzaj
Zbyt wiele intensywnych efektów parallax i pinning tworzy wrażenie przeciążenia i spowalnia stronę.
♿
prefers-reduced-motion
Szanuj ustawienia systemowe użytkownika. Zastępuj złożone animacje prostym fade lub wyłączaj je całkowicie.
🧭
Jasna nawigacja
Pinning sekcji powinien mieć jasny koniec. Dodaj przycisk "skip" do długich scroll-storytelling.
Interactive Demo — prefers-reduced-motion
Full Motion
Właściwości animacji
opacity: 0 → 1
translateY: 30px → 0
scale: 0.8 → 1
rotate: -8deg → 0
Implementacja w React / Next.js
Najczęstszy wzorzec to custom hook useReducedMotion i warunkowe renderowanie lżejszych wariantów animacji:
Tip: Testuj na urządzeniach mobilnych. Efekty, które wyglądają świetnie na desktopie, często frustrują na touch scrollu — szczególnie pinning i horizontal scroll.
03
Performance
Cztery zasady, których przestrzeganie gwarantuje płynne 60 fps niezależnie od wybranego stacku.
1Animuj transform i opacity
To jedyne właściwości CSS w pełni obsługiwane przez GPU na etapie composite. Zmiany top, left, width lub height wymuszają kosztowny cykl layout → paint → composite w każdej klatce.
Interactive Demo — Performance
GPU-compositedtransform + opacity
Layout-triggeringleft + width
💡
transform i opacity są obsługiwane przez GPU na etapie composite — przeglądarka nie musi przeliczać layoutu. left, width wymuszają layout → paint → composite w każdej klatce.
2Ogranicz liczbę observerów i handlerów
Dla scroll-triggered: IntersectionObserver zamiast ręcznego nasłuchiwania scroll event na każdej sekcji. Dla scroll-linked: jedna globalna pętla (GSAP ticker lub rAF) obsługująca wszystkie efekty.
3Throttling zamiast debouncing
Debounce przy ciągłym scrollu nigdy nie wyzwoli callbacka — animacja może się nie odpalić. Throttling gwarantuje wywołania co X ms. Jeszcze lepiej: pętla requestAnimationFrame zsynchronizowana z odświeżaniem ekranu.
Interactive Demo — rAF vs Throttle vs Debounce
↓ Scrolluj tutaj:
↕ Scrolluj, aby zobaczyć różnice w aktualizacjach
requestAnimationFrameupdates: 0
throttle(16ms)updates: 0
debounce(150ms)updates: 0
rAF — płynne, zsynchronizowane z odświeżaniem ekranu. Throttle — gwarantowane wywołania co X ms. Debounce — odpala dopiero po zakończeniu scrollowania — nigdy nie nadąża przy ciągłym scrollu.
4Minimalizacja DOM i kosztów layoutu
Prosta struktura DOM — jeden wrapper + elementy animowane — zmniejsza koszty layoutu. Logika manipuluje wyłącznie transform/opacity, a nie tworzy/usuwa elementów.
JavaScript
→
Style
→
Layout
→
Paint
→
Composite
transform/opacity pomijają Layout i Paint — trafiają prosto do Composite.
04
Architektura Next.js / React
Next.js z App Router wprowadza podział na Server i Client Components. Animacje scroll wymagają dostępu do window i document, więc zawsze muszą żyć w client components.
Client Components i SSR
Biblioteki animacyjne (GSAP, Framer Motion) ładuje się wyłącznie po stronie klienta. W Next.js najczęściej przez dynamic(import, { ssr: false }) lub oznaczenie komponentu dyrektywą "use client".
Wzorzec: dynamic import bez SSR
1importdynamicfrom'next/dynamic';
2
3// Komponent animacyjny ładowany tylko po stronie klienta
4constScrollAnimation=dynamic(
5()=>import('./ScrollAnimation'),
6{ssr:false}
7);
8
9// Server Component — może renderować layout
10exportdefaultfunctionHeroSection(){
11return(
12<section>
13<h1>HeroTitle</h1>
14<ScrollAnimation/>{/*Client-only*/}
15</section>
16);
17}
Lifecycle i cleanup animacji
Kluczowy problem w SPA: bez czyszczenia instancji po zmianie route'a, stare ScrollTriggery i observery wciąż nasłuchują i powodują wycieki pamięci.
Cleanup patterns
1import{useGSAP}from'@gsap/react';
2importgsapfrom'gsap';
3import{ScrollTrigger}from'gsap/ScrollTrigger';
4
5gsap.registerPlugin(ScrollTrigger);
6
7functionAnimatedSection(){
8constcontainerRef=useRef<HTMLDivElement>(null);
9
10 // useGSAP automatycznie czyści WSZYSTKIE animacje
11 // i ScrollTriggery stworzone wewnątrz callbacka
Bezpieczny wzorzec: serwer renderuje finalną pozycję elementu, a animacja startuje od drobnej różnicy (transform/opacity) dopiero po hydration. Dzięki temu nie ma layout shift i użytkownik widzi stabilny layout od razu.
Avoid: Nie animuj krytycznych elementów layoutu (szerokość, wysokość, margin) w sposób, który powoduje inny DOM/układ na serwerze vs kliencie — to prowadzi do hydration mismatch i CLS.
Prefer: Serwer renderuje element w finalnej pozycji z opacity: 1. Client component nakłada opacity: 0 + transform po mount i animuje do docelowego stanu.
05
GSAP + ScrollTrigger
GSAP daje największą kontrolę nad złożonymi scroll-linked animacjami — timelines, pinning, scrub, scrollerProxy. Oto kluczowe wzorce i pułapki.
Integracja z Lenis (smooth scroll)
Kanoniczna integracja GSAP + Lenis polega na zsynchronizowaniu obu silników w jednej pętli, tak aby ScrollTrigger reagował na wirtualny scroll Lenisa:
GSAP + Lenis — kanoniczna integracja
1importLenisfrom'lenis';
2importgsapfrom'gsap';
3import{ScrollTrigger}from'gsap/ScrollTrigger';
4
5gsap.registerPlugin(ScrollTrigger);
6
7// 1. Inicjalizacja Lenis
8constlenis=newLenis({duration:1.2});
9
10// 2. Podpięcie do ScrollTrigger
11lenis.on('scroll',ScrollTrigger.update);
12
13// 3. Synchronizacja z GSAP ticker
14gsap.ticker.add((time)=>{
15lenis.raf(time*1000);
16});
17
18// 4. Wyłączenie lag smoothing dla idealnej synchronizacji
19gsap.ticker.lagSmoothing(0);
Dzięki temu tylko GSAP ticker zarządza timem, a Lenis działa w jego ramach — zero podwójnych pętli, brak tearingu animacji.
quickTo — wydajne aktualizacje
gsap.quickTo pre-tworzy funkcję animującą daną właściwość. Zamiast tworzyć nowy tween 60 razy na sekundę, podajesz tylko nową wartość:
quickTo vs gsap.to
1// Pre-tworzenie settera — wykonaj RAZ
2constmoveX=gsap.quickTo('.element','x',{
3duration:0.3,
4ease:'power2.out',
5});
6
7// W scroll handler — tylko podaj wartość
8ScrollTrigger.create({
9onUpdate:(self)=>{
10moveX(self.progress*500);// ultra-wydajne
11},
12});
Interactive Demo — Scroll Easing
↓ Scrolluj
input: 0%
output: 0%
Preview
Easing Preset
Projektowanie triggerów
Typowy trigger: start: 'top bottom', end: 'bottom top' — animacja zaczyna się gdy wrapper wchodzi do dolnej krawędzi viewportu i kończy gdy go opuszcza od góry.
Tip: Posiadanie 5+ ScrollTriggerów per page jest normalne. Nie ma konieczności budowania jednego master-timelinu — ważniejsze jest spójne tworzenie i niszczenie ich w SPA/Next.
06
Framer Motion
Framer Motion (teraz Motion) jest idiomatyczny dla React/Next.js — integruje się z komponentami, lifecycle i warunkowym renderowaniem. Dwa tryby scroll animations:
Scroll-Triggered
whileInView
useAnimation + useInView
viewport prop
Scroll-Linked
useScroll
useTransform
scrollYProgress
whileInView — proste pojawianie się
Scroll-triggered z whileInView
1import{motion}from'framer-motion';
2
3functionFadeInCard({children}){
4return(
5<motion.div
6initial={{opacity:0,y:40}}
7whileInView={{opacity:1,y:0}}
8viewport={{once:true,margin:'-100px'}}
9transition={{
10duration:0.6,
11ease:[0.16,1,0.3,1],// custom ease-out
12}}
13>
14{children}
15</motion.div>
16);
17}
18
19// viewport options:
20// once: true — animacja odpala się raz
21// margin: '-100px' — trigger 100px wcześniej
22// amount: 0.5 — 50% elementu musi być widoczne
useScroll + useTransform — parallax i progress
Hook useScroll zwraca scrollYProgress (0–1), który useTransform mapuje na zakresy transformacji — bez re-renderów React, bo operuje na Motion Values.
Tip: Przy złożonych scenach grupuj elementy w jeden komponent ze wspólnym useScroll, zamiast wywoływać hook w wielu rozrzuconych komponentach.
07
Vanilla JS
Brak zależności, pełna kontrola, minimalny bundle. IntersectionObserver jako domyślny wybór dla triggerów, requestAnimationFrame dla scroll-linked.
IntersectionObserver — konfiguracja
IO API przyjmuje trzy parametry konfiguracyjne: root (domyślnie viewport), rootMargin (rozszerzenie/zawężenie obszaru obserwacji), threshold (próg widoczności 0–1).
Interactive Demo — IntersectionObserver
Box 1
Box 2
Box 3
Box 4
Box 5
Box 6
Live State
Box 1
intersecting: false
ratio: 0.00
Box 2
intersecting: false
ratio: 0.00
Box 3
intersecting: false
ratio: 0.00
Box 4
intersecting: false
ratio: 0.00
Box 5
intersecting: false
ratio: 0.00
Box 6
intersecting: false
ratio: 0.00
IO z CSS-only animacją
1// JavaScript — tylko toggle klasy
2constobserver=newIntersectionObserver(
3(entries)=>{
4entries.forEach(entry=>{
5entry.target.classList.toggle(
6'in-view',
7entry.isIntersecting
8);
9});
10},
11{
12threshold:0.3,
13rootMargin:'0px 0px -10% 0px',// trigger 10% wcześniej
14}
15);
16
17document.querySelectorAll('[data-animate]')
18.forEach(el=>observer.observe(el));
CSS — animacja działa bez JS runtime cost
1/* Domyślny stan — niewidoczny */
2[data-animate]{
3opacity:0;
4transform:translateY(20px);
5transition:opacity0.6sease-out,
6transform0.6sease-out;
7}
8
9/* Stan po dodaniu klasy przez IO */
10[data-animate].in-view{
11opacity:1;
12transform:translateY(0);
13}
14
15/* Respektuj preferencje użytkownika */
16@media(prefers-reduced-motion:reduce){
17[data-animate]{
18transition:opacity0.3sease-out;
19transform:none;
20}
21}
Scroll-linked: wzorzec rAF
Optymalny wzorzec: IO wykrywa aktywną sekcję, rAF uruchamia pętlę animacyjną tylko dla niej, a po wyjściu z viewportu pętla się zatrzymuje:
IO + rAF — hybrydowy wzorzec
1functioncreateScrollAnimation(element){
2letactive=false;
3letrafId=null;
4
5functionanimate(){
6if(!active)return;
7
8constrect=element.getBoundingClientRect();
9constviewH=window.innerHeight;
10constprogress=1-(rect.top/viewH);
11
12 // Bezpośrednia manipulacja DOM — zero frameworku
Proste efekty pojawiania się, minimalny bundle, restrykcyjny budżet JS
Szybka decyzja
Potrzebujesz pinning, scrub timeline lub scroll-hijacking?→ GSAP + ScrollTrigger
Tworzysz komponent React z prostym fade-in / parallax?→ Framer Motion
Budżet JS jest ograniczony, efekt to prosty fade/slide?→ Vanilla JS + CSS
Potrzebujesz smooth scroll z synchronizacją animacji?→ GSAP + Lenis
09
Checklista best practices
Podsumowanie kluczowych zasad do stosowania w każdym projekcie ze scroll animations.
✓
Animuj głównie transform i opacity
Unikaj layout-owych właściwości (top, left, width, height). Transform i opacity są GPU-composited.
✓
Używaj IntersectionObserver dla triggerów
whileInView (Framer), ScrollTrigger (GSAP) lub natywny IO. Nie nasłuchuj scroll event ręcznie na każdej sekcji.
✓
Jeden driver dla scroll-linked
ScrollTrigger ticker, useScroll hook lub własny requestAnimationFrame. Nie wiele rozproszonych pętli.
✓
Client components + cleanup w Next.js
Animacje w "use client" komponentach. useGSAP dla GSAP, observer.disconnect() w useEffect cleanup.
✓
Szanuj prefers-reduced-motion
Wyłączaj lub upraszczaj animacje dla użytkowników z nadwrażliwością na ruch. Oferuj fallback z prostym fade.
✓
Testuj na mobilkach
Performance + ergonomia touch scroll. Pinning i horizontal scroll często frustrują na urządzeniach dotykowych.
✓
Debuguj punkty start/end
GSAP: markers: true. Framer: wizualizuj scrollProgress jako tymczasowy progress bar.
✓
Projektuj animacje jak design system
Ograniczona liczba wzorców (fade-up, slide-in, sticky section) reużywanych w całym serwisie zamiast unikalnych hacków.
Antypatterns
✗gsap.to() w scroll handlerze — tworzy nowy tween 60x/s
✗setState() w scroll callback — powoduje re-render 60x/s
✗Debounce dla scroll-linked — animacja nigdy się nie odpali przy ciągłym scrollu
✗Animowanie width/height/top/left — wymusza layout recalc w każdej klatce
✗Brak cleanup w SPA — wycieki pamięci, duchy ScrollTriggerów po zmianie route
Podsumowanie: Dobre scroll animations sprowadzają się do trzech filarów — transform/opacity only, jeden driver + cleanup, szacunek dla użytkownika (reduced-motion, jasna nawigacja, testowanie na mobilkach).