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.
| 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() |
CultivaHero.vue — Landing page animada
<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>
ServicesSection.svelte — Stagger con cleanup
<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>
composables/useGSAP.ts — Carga lazy de plugins
💡 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.
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
// ✅ 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)
// ❌ 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.
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