Add Vue/Nuxt examples and extends the frameworks Skill

- Adds a Vue folder in the examples, with a Vue demo using the Composition API
- Adds a Nuxt folder in the examples, that includes a re-usable composable for importing and registering all the GSAP Plugins at a top level file and lazy-load plugins that are not extensively used in the app
- Modifies the SKILL.MD file in the gsap-frameworks folder to reflect the previous addition
This commit is contained in:
Rodrigo Hernando
2026-04-08 18:22:22 -04:00
parent 03d9f0c3db
commit 63353bf7a2
12 changed files with 402 additions and 5 deletions
+13 -1
View File
@@ -1,6 +1,6 @@
# GSAP skills — reference examples
Minimal demos that follow the skills exactly: transforms, autoAlpha, timelines, ScrollTrigger, and (in React) useGSAP with scope and cleanup.
Minimal demos that follow the skills exactly: transforms, autoAlpha, timelines, ScrollTrigger, and framework-specific patterns (useGSAP in React, onMount/onUnmount with gsap.context in Vue/Nuxt).
## Vanilla (HTML + JS)
@@ -15,3 +15,15 @@ Minimal demos that follow the skills exactly: transforms, autoAlpha, timelines,
- Patterns: `useGSAP()` with `scope: containerRef`, refs for targets, no selectors without scope; cleanup is automatic on unmount.
These examples are intended as reference implementations for AI agents and for quick manual verification of the skill patterns.
## Vue
- **examples/vue/** — Vite + Vue 3 + `<script setup>`.
- From repo root: `cd examples/vue && npm install && npm run dev`.
- Patterns: `gsap.context(() => {}, scope)` via `<script setup>`, `onMounted`/`onUnmounted` cleanup, ScrollTrigger on timeline, autoAlpha, stagger.
## Nuxt
- **examples/nuxt/** — Nuxt 4 with GSAP client-only re-usable composable.
- From repo root: `cd examples/nuxt && npm install && npm run dev`.
- Patterns: GSAP as client-only composable (`useGSAP.client.ts`), for gsap access, lazy-loading plugins, `gsap.context(() => {}, scope)` cleanup.
+3
View File
@@ -0,0 +1,3 @@
<template>
<NuxtPage />
</template>
@@ -0,0 +1,91 @@
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
const PLUGINS = [
"CSSRulePlugin",
"CustomBounce",
"CustomEase",
"CustomWiggle",
"Draggable",
"DrawSVGPlugin",
"EaselPlugin",
"EasePack",
"Flip",
"GSDevTools",
"InertiaPlugin",
"MorphSVGPlugin",
"MotionPathHelper",
"MotionPathPlugin",
"Observer",
"Physics2DPlugin",
"PhysicsPropsPlugin",
"PixiPlugin",
"ScrambleTextPlugin",
"ScrollSmoother",
"ScrollToPlugin",
"ScrollTrigger",
"SplitText",
"TextPlugin",
] as const;
type Plugins = typeof PLUGINS[number];
// In order to dynamically load all the GSAP plugins
const pluginMap = {
CustomEase: () => import("gsap/CustomEase"),
Draggable: () => import("gsap/Draggable"),
CSSRulePlugin: () => import("gsap/CSSRulePlugin"),
EaselPlugin: () => import("gsap/EaselPlugin"),
EasePack: () => import("gsap/EasePack"),
Flip: () => import("gsap/Flip"),
MotionPathPlugin: () => import("gsap/MotionPathPlugin"),
Observer: () => import("gsap/Observer"),
PixiPlugin: () => import("gsap/PixiPlugin"),
ScrollToPlugin: () => import("gsap/ScrollToPlugin"),
ScrollTrigger: () => import("gsap/ScrollTrigger"),
TextPlugin: () => import("gsap/TextPlugin"),
DrawSVGPlugin: () => import("gsap/DrawSVGPlugin"),
Physics2DPlugin: () => import("gsap/Physics2DPlugin"),
PhysicsPropsPlugin: () => import("gsap/PhysicsPropsPlugin"),
ScrambleTextPlugin: () => import("gsap/ScrambleTextPlugin"),
CustomBounce: () => import("gsap/CustomBounce"),
CustomWiggle: () => import("gsap/CustomWiggle"),
GSDevTools: () => import("gsap/GSDevTools"),
InertiaPlugin: () => import("gsap/InertiaPlugin"),
MorphSVGPlugin: () => import("gsap/MorphSVGPlugin"),
MotionPathHelper: () => import("gsap/MotionPathHelper"),
ScrollSmoother: () => import("gsap/ScrollSmoother"),
SplitText: () => import("gsap/SplitText"),
} as const;
type PluginMap = typeof pluginMap;
type Plugins = keyof PluginMap;
// Resolves the module type for a given key, then picks the named export matching the key
// this allows to have the type definitions for autocomplete in your code editor
type PluginModule<K extends Plugins> = Awaited<ReturnType<PluginMap[K]>>;
type PluginExport<K extends Plugins> = PluginModule<K>[K & keyof PluginModule<K>];
export default function() {
// Register all the GSAP Plugins you want at this point
gsap.registerPlugin(ScrollTrigger);
/*
If you want to lazy load some of the plugins that are
not widely used in your app (for example in just a couple
of components or a single route), you can use this method
*/
async function lazyLoadPlugin<K extends Plugins>(plugin: K): Promise<PluginExport<K>> {
const loader = pluginMap[plugin];
const m = await loader();
const p = (m as any)[plugin];
gsap.registerPlugin(p);
return p;
}
return {
gsap,
ScrollTrigger,
lazyLoadPlugin,
};
};
+56
View File
@@ -0,0 +1,56 @@
<script setup>
import { onMounted, onUnmounted } from "vue";
import useGSAP from "./composables/useGSAP.client";
const { gsap, ScrollTrigger, lazyLoadPlugin } = useGSAP();
let ctx;
onMounted(async () => {
const SplitText = await lazyLoadPlugin("SplitText");
ctx = gsap.context(() => {
const split = SplitText.create("h1", {
type: "chars",
});
gsap.from(split.chars, {
autoAlpha: 0,
y: -50,
xPercent: -100,
rotation: -45,
ease: "power1.inOut",
stagger: {
amount: 0.3,
},
});
gsap.set("h1", { autoAlpha: 1 });
});
});
onMounted(() => {
ctx && ctx.revert();
});
</script>
<template>
<main>
<h1>GSAP SplitText</h1>
</main>
</template>
<style scoped>
main {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif;
font-size: 18px;
}
main h1 {
opacity: 0;
visibility: hidden;
}
</style>
+4
View File
@@ -0,0 +1,4 @@
export default defineNuxtConfig({
ssr: true,
compatibilityDate: '2025-07-15',
});
+18
View File
@@ -0,0 +1,18 @@
{
"name": "gsap-nuxt-example",
"type": "module",
"private": true,
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"dependencies": {
"gsap": "^3.14.2",
"nuxt": "^4.4.2",
"vue": "^3.5.30",
"vue-router": "^5.0.4"
}
}
+47
View File
@@ -0,0 +1,47 @@
/**
* Minimal GSAP Vue 3 example — follows gsap-frameworks skill: gsap.context, scope, cleanup.
* Scoped selectors via context; automatic revert on unmount.
*/
<script setup>
import { ref, onMounted, onUnmounted } from "vue";
import { gsap } from "gsap";
const container = ref(null);
let ctx;
onMounted(() => {
ctx = gsap.context(() => {
// Simple tween — moves the box right and fades it in
gsap.to(".box", { x: 100, autoAlpha: 1, duration: 0.6, ease: "power2" });
// Stagger animation on .item elements (scoped to container)
gsap.from(".item", { autoAlpha: 0, y: 20, stagger: 0.1 });
// ScrollTrigger timeline on the tall section
gsap.timeline({
scrollTrigger: { trigger: ".scroll-section", start: "top center", end: "bottom center", scrub: true },
}).to(".scroll-box", { x: 200, rotation: 360 });
}, container.value); // scope — all selectors resolve inside container
});
onUnmounted(() => {
ctx?.revert(); // cleanup: reverts all GSAP animations created in this context
});
</script>
<template>
<div ref="container" :style="{ padding: '2rem' }">
<h1>GSAP Vue 3 — gsap.context + scope + cleanup</h1>
<div class="box" :style="{ width: '80px', height: '80px', background: '#0fa', borderRadius: '8px', marginBottom: '1rem', visibility: 'hidden' }" />
<div class="item" :style="{ margin: '0.5rem 0' }">Item 1</div>
<div class="item" :style="{ margin: '0.5rem 0' }">Item 2</div>
<div class="item" :style="{ margin: '0.5rem 0' }">Item 3</div>
<div class="scroll-section" :style="{ height: '150vh', marginTop: '2rem', background: '#f0f0f0', padding: '1rem' }">
<p>Scroll down to trigger the timeline</p>
<div class="scroll-box" :style="{ width: '60px', height: '60px', background: '#f0a', borderRadius: '8px', marginTop: '1rem' }" />
</div>
</div>
</template>
+15
View File
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GSAP Vue 3 — gsap.context + scope + cleanup</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/main.js"></script>
</body>
</html>
+9
View File
@@ -0,0 +1,9 @@
import { createApp } from "vue";
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import App from "./App.vue";
// Register all the GSAP Plugins you want only once at this top level file
gsap.registerPlugin(ScrollTrigger);
createApp(App).mount("#app");
+19
View File
@@ -0,0 +1,19 @@
{
"name": "gsap-vue-example",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"gsap": "^3.14.2",
"vue": "^3.5.32"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.5",
"vite": "^8.0.4"
}
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
});
+120 -4
View File
@@ -20,6 +20,8 @@ Apply when writing or reviewing GSAP code in Vue (or Nuxt), Svelte (or SvelteKit
## Vue 3 (Composition API)
See `examples/vue/` for a runnable Vite + Vue 3 project demonstrating these patterns.
Use **onMounted** to run GSAP after the component is in the DOM. Use **onUnmounted** to clean up.
```javascript
@@ -46,7 +48,7 @@ export default {
});
return { container };
}
},
};
```
@@ -87,6 +89,120 @@ onUnmounted(() => {
</template>
```
## Nuxt 3
> See `examples/nuxt/` for a runnable Nuxt 4 project with plugin registration, lazy loading, and SSR-safe patterns.
Use a **client-only composable** (`.client.ts` suffix) to register GSAP Plugins, it never runs on the server:
```typescript
// composables/useGSAP.client.ts
import { gsap } from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
const PLUGINS = [
"CSSRulePlugin",
"CustomBounce",
"CustomEase",
"CustomWiggle",
"Draggable",
"DrawSVGPlugin",
"EaselPlugin",
"EasePack",
"Flip",
"GSDevTools",
"InertiaPlugin",
"MorphSVGPlugin",
"MotionPathHelper",
"MotionPathPlugin",
"Observer",
"Physics2DPlugin",
"PhysicsPropsPlugin",
"PixiPlugin",
"ScrambleTextPlugin",
"ScrollSmoother",
"ScrollToPlugin",
"ScrollTrigger",
"SplitText",
"TextPlugin",
] as const;
type Plugins = (typeof PLUGINS)[number];
// In order to dynamically load all the GSAP plugins
const pluginMap = {
CustomEase: () => import("gsap/CustomEase"),
Draggable: () => import("gsap/Draggable"),
CSSRulePlugin: () => import("gsap/CSSRulePlugin"),
EaselPlugin: () => import("gsap/EaselPlugin"),
EasePack: () => import("gsap/EasePack"),
Flip: () => import("gsap/Flip"),
MotionPathPlugin: () => import("gsap/MotionPathPlugin"),
Observer: () => import("gsap/Observer"),
PixiPlugin: () => import("gsap/PixiPlugin"),
ScrollToPlugin: () => import("gsap/ScrollToPlugin"),
ScrollTrigger: () => import("gsap/ScrollTrigger"),
TextPlugin: () => import("gsap/TextPlugin"),
DrawSVGPlugin: () => import("gsap/DrawSVGPlugin"),
Physics2DPlugin: () => import("gsap/Physics2DPlugin"),
PhysicsPropsPlugin: () => import("gsap/PhysicsPropsPlugin"),
ScrambleTextPlugin: () => import("gsap/ScrambleTextPlugin"),
CustomBounce: () => import("gsap/CustomBounce"),
CustomWiggle: () => import("gsap/CustomWiggle"),
GSDevTools: () => import("gsap/GSDevTools"),
InertiaPlugin: () => import("gsap/InertiaPlugin"),
MorphSVGPlugin: () => import("gsap/MorphSVGPlugin"),
MotionPathHelper: () => import("gsap/MotionPathHelper"),
ScrollSmoother: () => import("gsap/ScrollSmoother"),
SplitText: () => import("gsap/SplitText"),
} as const;
type PluginMap = typeof pluginMap;
type Plugins = keyof PluginMap;
// Resolves the module type for a given key, then picks the named export matching the key
// this allows to have the type definitions for autocomplete in your code editor
type PluginModule<K extends Plugins> = Awaited<ReturnType<PluginMap[K]>>;
type PluginExport<K extends Plugins> = PluginModule<K>[K & keyof PluginModule<K>];
export default function () {
// Register all the GSAP Plugins you want at this point
gsap.registerPlugin(ScrollTrigger);
/*
If you want to lazy load some of the plugins that are
not widely used in your app (for example in just a couple
of components or a single route), you can use this method
*/
async function lazyLoadPlugin<K extends Plugins>(plugin: K): Promise<PluginExport<K>> {
const loader = pluginMap[plugin];
const m = await loader();
const p = (m as any)[plugin];
gsap.registerPlugin(p);
return p;
}
return {
gsap,
ScrollTrigger,
lazyLoadPlugin,
};
}
```
Access in components via `useGSAP()`:
```javascript
import useGSAP from "./composables/useGSAP.client";
const { gsap, ScrollTrigger, lazyLoadPlugin } = useGSAP();
```
- ✅ **`.client.ts` suffix** ensures the composable only runs in the browser (SSR-safe). Never import GSAP in server-rendered code.
- ✅ **`useGSAP()`** provides typed access to the gsap instance and lazy load method.
- ✅ **Lazy-load any plugin** (SplitText, MorphSVG, etc.) that is not widely used in your app to reduce initial bundle size.
- ✅ Use **gsap.context(scope)** and **onUnmounted → ctx.revert()** in components, same as Vue 3.
## Svelte
Use **onMount** to run GSAP after the DOM is ready. Use the **returned cleanup function** from onMount (or track the context and clean up in a reactive block / component destroy) to revert. Svelte 5 uses a different lifecycle; the same principle applies: create in “mounted” and revert in “destroyed.”
@@ -134,9 +250,9 @@ ScrollTrigger instances are created when you use the `scrollTrigger` config on a
## When to Create vs Kill
| Lifecycle | Action |
|-----------------|--------|
| **Mounted** | Create tweens and ScrollTriggers inside **gsap.context(scope)**. |
| Lifecycle | Action |
| --------------------- | ----------------------------------------------------------------------------------------------------------------- |
| **Mounted** | Create tweens and ScrollTriggers inside **gsap.context(scope)**. |
| **Unmount / Destroy** | Call **ctx.revert()** so all animations and ScrollTriggers in that context are killed and inline styles reverted. |
Do not create GSAP animations in the component’s setup or in a synchronous top-level script that runs before the root element exists. Wait for **onMounted** / **onMount** (or equivalent) so the container ref is in the DOM.