Skill • Web / GSAP

GSAP × Vue 3, Nuxt 4 y Svelte

Patrones de referencia para integrar animaciones GSAP en frameworks no-React: lifecycle correcto, scoping de selectores y cleanup sin memory leaks.

Vue 3 Nuxt 4 Svelte / SvelteKit GSAP 3 Premium · 5€
Principios Universales
⏱️

Crear tras el montaje

Los tweens y ScrollTriggers siempre se crean después de que el DOM esté disponible: onMounted en Vue, onMount en Svelte. Nunca en setup sincrónico.

🧹

Limpiar al desmontar

Siempre llamar ctx.revert() en onUnmounted (Vue) o en el return de onMount (Svelte). Mata tweens + ScrollTriggers + revierte estilos inline.

🎯

Acotar selectores

Pasar el elemento raíz como segundo argumento de gsap.context(cb, scope) para que .box sólo afecte al componente actual, no a toda la página.

Tabla de Lifecycle
Framework Hook "Crear" Hook "Limpiar" Patrón de cleanup
Vue 3 Options mounted() unmounted() this.ctx?.revert()
Vue 3 Composition onMounted() onUnmounted() ctx?.revert()
Nuxt 4 onMounted() onUnmounted() ctx?.revert() + useGSAP() composable
Svelte 4 onMount() return () => ... return () => ctx.revert()
Svelte 5 $effect() return () => ... return () => ctx.revert()
Vue 3 — Composition API (script setup)
V

CultivaHero.vue — Landing page animada

Hero section con stagger de tarjetas de servicios + cleanup correcto
components/CultivaHero.vue
Vue 3
<script setup>
import { onMounted, onUnmounted, ref } from "vue"
import { gsap } from "gsap"
// ScrollTrigger se registra UNA VEZ en main.js — no aquí

const container = ref(null)
let ctx

onMounted(() => {
  if (!container.value) return

  // gsap.context(scope) → selectores ".title", ".card" se acotan
  // al elemento container.value, no a toda la página
  ctx = gsap.context(() => {

    // Hero title entra desde abajo
    gsap.from(".title", {
      autoAlpha: 0,
      y: 40,
      duration: 0.8,
      ease: "power3.out"
    })

    // Tarjetas de servicios con stagger
    gsap.from(".service-card", {
      autoAlpha: 0,
      y: 30,
      stagger: 0.12,
      delay: 0.4,
      duration: 0.6,
      ease: "back.out(1.4)"
    })

    // ScrollTrigger: contadores se activan al hacer scroll
    gsap.from(".stat-number", {
      textContent: 0,
      duration: 1.5,
      snap: { textContent: 1 },
      scrollTrigger: {
        trigger: ".stats-section",
        start: "top 80%"
      }
    })

  }, container.value)  // ← segundo arg = scope del componente
})

// ✅ CRÍTICO: revert mata todos los tweens y ScrollTriggers del contexto
onUnmounted(() => {
  ctx?.revert()
})
</script>

<template>
  <section ref="container" class="hero">
    <h1 class="title">Automatiza tu negocio con IA</h1>

    <div class="services-grid">
      <div v-for="s in services" class="service-card">
        {{ s.name }}
      </div>
    </div>

    <div class="stats-section">
      <span class="stat-number">142</span> clientes
    </div>
  </section>
</template>
gsap.context(cb, container.value) — el segundo argumento aísla todos los selectores dentro de este componente, evitando colisiones con otras instancias de la misma página.
onUnmounted → ctx?.revert() — mata todos los tweens activos, cancela ScrollTriggers y revierte los estilos inline para que el garbage collector pueda liberar la memoria.
Svelte / SvelteKit
S

ServicesSection.svelte — Stagger con cleanup

onMount con función de retorno para cleanup automático al destruir el componente
components/ServicesSection.svelte
Svelte 4
<script>
  import { onMount } from "svelte"
  import { gsap } from "gsap"
  import { ScrollTrigger } from "gsap/ScrollTrigger"

  let container;  // bind:this lo asigna al montar

  onMount(() => {
    if (!container) return;

    const ctx = gsap.context(() => {
      gsap.from(".card", {
        autoAlpha: 0,
        y: 30,
        stagger: 0.1,
        ease: "power2.out",
        scrollTrigger: {
          trigger: container,
          start: "top 75%"
        }
      })
    }, container)

    // Svelte: onMount puede retornar función de limpieza
    return () => ctx.revert()  // ← se llama al destruir el componente
  })
</script>

<section bind:this={container}>
  {#each services as service}
    <div class="card">{service.name}</div>
  {/each}
</section>
bind:this={container} — obtiene la referencia al elemento raíz; se pasa como scope a gsap.context() para acotar los selectores.
return () => ctx.revert() — Svelte ejecuta esta función cuando el componente se destruye. Incluye cleanup de todos los ScrollTriggers creados dentro del contexto.
Nuxt 4 — Composable reutilizable
N

composables/useGSAP.ts — Carga lazy de plugins

Centraliza el registro de plugins y permite lazy-loading para reducir el bundle inicial

💡 Por qué un composable

En Nuxt, el registro de plugins puede ocurrir en SSR (lado servidor) donde no hay DOM. El composable centraliza la lógica: registra una vez en cliente y expone un método lazyLoadPlugin() para plugins de uso esporádico (SplitText, MorphSVG) sin aumentar el bundle principal.

composables/useGSAP.ts
TypeScript
import { gsap } from "gsap"
import { ScrollTrigger } from "gsap/ScrollTrigger"

const pluginMap = {
  ScrollTrigger: () => import("gsap/ScrollTrigger"),
  SplitText:    () => import("gsap/SplitText"),
  Flip:         () => import("gsap/Flip"),
  CustomEase:   () => import("gsap/CustomEase"),
  TextPlugin:   () => import("gsap/TextPlugin"),
} as const

type Plugins = keyof typeof pluginMap

export default function useGSAP() {
  // ✅ Se registra en cliente, una sola vez
  gsap.registerPlugin(ScrollTrigger)

  // Lazy-load para plugins pesados o de uso esporádico
  async function lazyLoadPlugin<K extends Plugins>(plugin: K) {
    const m = await pluginMap[plugin]()
    const p = (m as any)[plugin]
    gsap.registerPlugin(p)
    return p
  }

  return { gsap, ScrollTrigger, lazyLoadPlugin }
}

// Uso en un componente Nuxt:
// const { gsap, lazyLoadPlugin } = useGSAP()
// const SplitText = await lazyLoadPlugin('SplitText')  ← carga solo cuando se necesita
Do's and Don'ts
✅ Correcto
// ✅ Dentro de onMounted, con scope
onMounted(() => {
  ctx = gsap.context(() => {
    gsap.from(".box", { y: 20 })
  }, container.value)
})

// ✅ Cleanup en onUnmounted
onUnmounted(() => ctx?.revert())

// ✅ Plugins en main.js (una vez)
gsap.registerPlugin(ScrollTrigger)
❌ Incorrecto
// ❌ En setup sin onMounted (DOM no existe)
const container = ref(null)
gsap.to(container.value, { x: 100 })  // null!

// ❌ Sin scope — afecta a toda la página
gsap.context(() => {
  gsap.from(".box", { y: 20 })
})  // ← falta el segundo arg

// ❌ Sin cleanup — memory leak
onMounted(() => {
  gsap.to(".hero", { opacity: 1 })
})  // ScrollTriggers quedan vivos

🔄 ScrollTrigger & Cleanup

Los ScrollTriggers creados dentro de un gsap.context() se incluyen automáticamente en el revert. No es necesario destruirlos manualmente.

Vue 3 — ScrollTrigger con refresh tras carga de datos
Vue 3
import { nextTick } from "vue"

// ✅ Después de cargar datos async y actualizar DOM
const loadData = async () => {
  services.value = await fetchServices()
  await nextTick()            // esperar que Vue actualice el DOM
  ScrollTrigger.refresh()   // recalcular posiciones de triggers
}

// Los ScrollTriggers dentro del ctx se limpian solos:
onUnmounted(() => ctx?.revert())  // ← mata tweens + ScrollTriggers