mirror of
https://github.com/greensock/gsap-skills
synced 2026-09-22 23:34:24 +08:00
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:
+13
-1
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
@@ -0,0 +1,4 @@
|
||||
export default defineNuxtConfig({
|
||||
ssr: true,
|
||||
compatibilityDate: '2025-07-15',
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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");
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
});
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user