@mehshekari/gsap-motion 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,153 +0,0 @@
1
- # Frameworks other than React
2
-
3
- GSAP has no framework. What changes between frameworks is only **where an
4
- animation is created and where it is torn down** — the lifecycle. Everything
5
- else in this skill applies unchanged: motion design, timelines, ScrollTrigger,
6
- SVG, text, Flip, performance and accessibility.
7
-
8
- One pattern sits under every framework below:
9
-
10
- 1. Create animations inside `gsap.context(fn, root)`, so selector strings
11
- resolve inside the component's own root element.
12
- 2. Build the reduced-motion branch with `gsap.matchMedia()` inside that context,
13
- so it goes with it.
14
- 3. Call `ctx.revert()` when the component goes away. That undoes every tween,
15
- ScrollTrigger, SplitText and matchMedia created inside, and restores inline
16
- styles.
17
-
18
- Timers, raw `addEventListener` calls and observers are not GSAP's — clear them
19
- yourself in the same cleanup.
20
-
21
- ## Vanilla JavaScript
22
-
23
- ```js
24
- import gsap from "gsap";
25
- import { ScrollTrigger } from "gsap/ScrollTrigger";
26
-
27
- gsap.registerPlugin(ScrollTrigger);
28
-
29
- export function mountReveal(root) {
30
- const ctx = gsap.context(() => {
31
- const mm = gsap.matchMedia();
32
- mm.add(
33
- {
34
- motion: "(prefers-reduced-motion: no-preference)",
35
- reduced: "(prefers-reduced-motion: reduce)",
36
- },
37
- ({ conditions }) => {
38
- if (conditions.reduced) {
39
- gsap.set("[data-item]", { autoAlpha: 1, y: 0 });
40
- return;
41
- }
42
- gsap.from("[data-item]", {
43
- autoAlpha: 0,
44
- y: 24,
45
- stagger: { amount: 0.4 },
46
- scrollTrigger: { trigger: root, start: "top 75%" },
47
- });
48
- },
49
- );
50
- }, root);
51
-
52
- return () => ctx.revert(); // call when root is removed
53
- }
54
- ```
55
-
56
- A static page that never removes the element can skip the cleanup. Anything that
57
- swaps content — a client-side router, HTMX, Turbo, a modal — cannot.
58
-
59
- ## Vue 3 and Nuxt
60
-
61
- ```vue
62
- <script setup>
63
- import gsap from "gsap";
64
- import { onMounted, onUnmounted, ref } from "vue";
65
-
66
- const root = ref(null);
67
- let ctx;
68
-
69
- onMounted(() => {
70
- ctx = gsap.context(() => {
71
- gsap.from("[data-item]", { autoAlpha: 0, y: 24, stagger: 0.06 });
72
- }, root.value);
73
- });
74
-
75
- onUnmounted(() => ctx?.revert());
76
- </script>
77
-
78
- <template>
79
- <section ref="root">…</section>
80
- </template>
81
- ```
82
-
83
- - `onMounted` never runs during server rendering, so this is safe under Nuxt as
84
- written.
85
- - Animation that reacts to state belongs in a `watch` that adds its tweens to
86
- the same context with `ctx.add(() => { … })`, so they are reverted with it.
87
-
88
- ## Svelte
89
-
90
- ```svelte
91
- <script>
92
- import gsap from "gsap";
93
- import { onMount } from "svelte";
94
-
95
- let root;
96
-
97
- onMount(() => {
98
- const ctx = gsap.context(() => {
99
- gsap.from("[data-item]", { autoAlpha: 0, y: 24, stagger: 0.06 });
100
- }, root);
101
- return () => ctx.revert();
102
- });
103
- </script>
104
-
105
- <section bind:this={root}>…</section>
106
- ```
107
-
108
- A function returned from `onMount` runs when the component is destroyed.
109
- `onMount` does not run during server rendering. The markup uses Svelte's
110
- classic syntax; check how your Svelte version declares a `bind:this` variable
111
- before copying it.
112
-
113
- ## Astro
114
-
115
- An Astro `<script>` runs once. With view transitions enabled, the page is
116
- swapped without a reload, so build on each page load and revert before each
117
- swap:
118
-
119
- ```astro
120
- <script>
121
- import gsap from "gsap";
122
-
123
- let ctx;
124
-
125
- document.addEventListener("astro:page-load", () => {
126
- ctx = gsap.context(() => {
127
- gsap.from("[data-item]", { autoAlpha: 0, y: 24, stagger: 0.06 });
128
- });
129
- });
130
-
131
- document.addEventListener("astro:before-swap", () => ctx?.revert());
132
- </script>
133
- ```
134
-
135
- Inside a React, Vue or Svelte island, use that framework's section instead.
136
-
137
- ## Mounting twice
138
-
139
- Selector strings are scoped to the context's root, but `motionPath.path` and
140
- `morphSVG.shape` are resolved by the plugins against the whole document. A
141
- component rendered twice with a hardcoded id drives the first instance's
142
- geometry. Generate an id per instance in every framework, not only React.
143
-
144
- ## What the audit covers here
145
-
146
- - `audit-gsap.mjs` reads `.js`, `.ts`, `.jsx`, `.tsx` and `.mjs` files only.
147
- Logic inside `.vue`, `.svelte` or `.astro` files is not scanned — keep the
148
- animation in a sibling module, like `mountReveal` above, if you want it
149
- checked.
150
- - Its React-specific rules — `orphan-tween`, `state-per-event` and
151
- `shared-plugin-id` — apply only to React files. The rest apply to every file
152
- it reads: cost, loops, scrub, listeners, unmanaged instances, plugin
153
- registration, reduced motion and shipped dev tooling.
@@ -1,234 +0,0 @@
1
- # React and Next.js
2
-
3
- Written for the Next.js App Router with React 19 and Server Components, and
4
- verified against `@gsap/react` 2.1. Most of it applies to any React app; the
5
- RSC sections apply only where Server Components exist. Animation is always a
6
- client concern, and the interesting problems are all lifecycle. Not using
7
- React? Read [frameworks.md](frameworks.md) instead.
8
-
9
- ## `useGSAP`
10
-
11
- `useGSAP`, from `@gsap/react`, is `useLayoutEffect` with an SSR fallback
12
- plus automatic `gsap.context()` cleanup — which is what makes it survive React
13
- 19 StrictMode's double invoke.
14
-
15
- ```ts
16
- "use client";
17
-
18
- import { useGSAP } from "@gsap/react";
19
- import gsap from "gsap";
20
- import { useRef } from "react";
21
-
22
- gsap.registerPlugin(useGSAP); // module scope, once
23
-
24
- export function useThing() {
25
- const root = useRef<HTMLDivElement>(null);
26
-
27
- useGSAP(
28
- () => {
29
- gsap.from("[data-item]", { y: 20, stagger: 0.05 }); // scoped to root
30
- return () => { /* anything the context does not own */ };
31
- },
32
- { scope: root },
33
- );
34
-
35
- return root;
36
- }
37
- ```
38
-
39
- `{ scope: root }` is not optional in practice: it makes selector strings resolve
40
- inside the component, so two instances on a page do not animate each other's
41
- elements.
42
-
43
- Options: `scope`, `dependencies`, `revertOnUpdate`. With `dependencies`, the
44
- body re-runs when they change; `revertOnUpdate: true` also reverts the previous
45
- run first.
46
-
47
- Being a layout effect matters: it runs **before paint**, so a `from` or a `set`
48
- lands without a flash of the end state.
49
-
50
- ## `contextSafe`
51
-
52
- Anything that creates a tween *outside* the hook body — an event handler, a
53
- promise, a callback — is created outside the context and never reverted.
54
-
55
- ```ts
56
- const { contextSafe } = useGSAP(() => { ... }, { scope: root });
57
-
58
- const onEnter = contextSafe(() => {
59
- gsap.to(el.current, { scale: 1.05 }); // now owned by the context
60
- });
61
- ```
62
-
63
- Two things to get right:
64
-
65
- - **Only wrap functions that create animations.** Playing a timeline that
66
- already exists creates nothing and needs no wrapper.
67
- - **`contextSafe` runs during render.** Reading a ref inside the callback you
68
- pass it trips React's "cannot access refs during render" lint rule. Build the
69
- timeline inside the hook body, keep it on a ref, and expose a plain
70
- `useCallback` that plays it.
71
-
72
- ## Refs, never state
73
-
74
- React state for a value that changes every frame is a re-render per frame.
75
-
76
- ```ts
77
- // Wrong
78
- const [x, setX] = useState(0);
79
- onMouseMove = (e) => setX(e.clientX);
80
-
81
- // Right
82
- const moveX = gsap.quickTo(el.current, "x", { duration: 0.5, ease: "power3" });
83
- onMouseMove = (e) => moveX(e.clientX);
84
- ```
85
-
86
- State is for *what exists* — is the modal open, which tab is active. GSAP owns
87
- *how it moves*. Crossing that line is the most common performance bug in
88
- animated React.
89
-
90
- ## The RSC boundary
91
-
92
- A component that animates is `"use client"`. Keep it a leaf so the page above it
93
- stays a Server Component.
94
-
95
- **If the target is currently a Server Component, converting it is part of the
96
- cost, and you have to price it before you design.** Two questions:
97
-
98
- 1. *Is the library already on this route?* Grep for existing GSAP use in the
99
- sections the page renders. If a sibling already loads `gsap` and
100
- `ScrollTrigger`, the marginal cost is this component's own JS — a kilobyte or
101
- two — rather than thirty-odd. That usually settles the argument. If nothing
102
- on the route loads GSAP yet, this animation is buying the whole library for
103
- one reveal, and it had better be worth it.
104
- 2. *Does anything claim otherwise?* A comment saying the section ships no client
105
- JavaScript stops being true the moment you add the hook. Update it in the
106
- same change — a stale claim in a docblock is worse than none, because the
107
- next person believes it.
108
-
109
- State both in the Notes section of your answer. "It becomes a client component"
110
- is a fact the reader needs, not a detail.
111
-
112
- ### Reaching the element
113
-
114
- A shared component that does not forward props has nowhere to hang a `data-*`
115
- attribute. Widening it to take the rest of its element's props is usually right
116
- and is often already the house pattern — check a sibling before inventing one.
117
- Finding the element positionally instead (`row.firstElementChild`,
118
- `querySelectorAll("path")[3]`) is a selector that breaks silently the next time
119
- the markup moves.
120
-
121
- Two consequences worth planning for:
122
-
123
- - **Data and copy come down as props.** A client component importing a content
124
- or locale module bundles every locale into the browser payload. Resolve on the
125
- server, pass what the visitor needs. The same applies to large generated data
126
- — path tables, keyframe sets: hand them down rather than importing them
127
- client-side.
128
- - **`"use client"` module scope still runs during SSR.** Registering plugins
129
- there is fine — GSAP defers anything touching `window` — but reading `window`
130
- or `document` at module scope is not.
131
-
132
- ## Reading the environment
133
-
134
- Read direction and preferences from the DOM inside the effect, not from a
135
- prop or a locale constant:
136
-
137
- ```ts
138
- const isRtl = document.documentElement.dir === "rtl";
139
- ```
140
-
141
- The layout stamps `dir` before first paint, and reading it there keeps the hook
142
- independent of the i18n library.
143
-
144
- ## Cleanup: what the context already owns
145
-
146
- Wider than it is usually given credit for, and worth knowing precisely —
147
- otherwise you write cleanup that does nothing and still miss the real leaks.
148
-
149
- While a context's function runs, GSAP points `_context` at that context, and
150
- these register themselves with it on construction. `context.revert()` then
151
- reverts them:
152
-
153
- - tweens and timelines
154
- - `ScrollTrigger`
155
- - `Observer`
156
- - `Draggable`
157
- - `SplitText`
158
- - `gsap.matchMedia()`
159
-
160
- So inside a `useGSAP` body, none of those need a manual `.kill()` or
161
- `.revert()`. An explicit one is redundant rather than wrong.
162
-
163
- **These are genuinely yours**, because GSAP never sees them:
164
-
165
- - `setTimeout` / `setInterval` — clear them
166
- - `addEventListener` on `window` or `document` — remove them
167
- - `ResizeObserver` / `IntersectionObserver` — `.disconnect()`
168
- - Three.js geometries, materials, textures — dispose them
169
- - **anything created outside the context body** — module scope, a plain
170
- `useEffect`, or a handler that never went through `contextSafe`. That last one
171
- is the real leak, and the one people write cleanup for the wrong things
172
- instead of catching.
173
-
174
- ```ts
175
- useGSAP(() => {
176
- const mm = gsap.matchMedia(); // reverted with the context
177
- const io = new ResizeObserver(onResize); // not — disconnect it
178
- mm.add({ ... }, () => { ... });
179
- io.observe(root.current!);
180
- return () => io.disconnect();
181
- }, { scope: root });
182
- ```
183
-
184
- `node .claude/skills/gsap-creative-animation/scripts/audit-gsap.mjs <path>`
185
- checks this distinction, among others.
186
-
187
- ## Unique ids
188
-
189
- A scope does **not** reach inside plugin config. `motionPath.path` and
190
- `morphSVG.shape` are resolved by the plugin against the whole document, so a
191
- hardcoded id in a component rendered twice makes both instances drive the first
192
- one's geometry — silently.
193
-
194
- ```ts
195
- const uid = `rs${useId().replace(/[^a-zA-Z0-9]/g, "")}`;
196
- ```
197
-
198
- React's generated id carries delimiters a CSS selector cannot hold, so strip it
199
- to word characters. It is stable across server and client, which a counter is
200
- not. Then `id={`${uid}-track`}` in the markup and `#${uid}-track` in the config.
201
-
202
- ## Route transitions
203
-
204
- The App Router unmounts the old tree before the new one paints, so an exit
205
- animation has nowhere to play. Options, in order of how much they cost:
206
-
207
- 1. **Animate entrance only.** Keyed off `usePathname()`. Honest, cheap, and what
208
- most sites actually want.
209
- 2. **An overlay that outlives the route.** Mounted in the layout, so it is not
210
- unmounted by the navigation. See [page-transition](../preset/page-transition.md).
211
- 3. **`router.push` behind a played-out timeline.** Intercept, play the exit,
212
- navigate on complete. Breaks the back button's feel — use sparingly.
213
-
214
- After any navigation, call `ScrollTrigger.refresh()` once the new layout has
215
- settled.
216
-
217
- ## Hydration
218
-
219
- Anything read from `localStorage`, `sessionStorage`, or `matchMedia` during
220
- render makes the server and client disagree. Render the neutral state, then
221
- correct it in the layout effect — before paint, so nothing is seen.
222
-
223
- ## Failures
224
-
225
- | Symptom | Cause |
226
- |---|---|
227
- | Animation runs twice in dev | tweens created outside `useGSAP` — StrictMode |
228
- | Animation leaks after navigation | ScrollTrigger or listener created outside the scope |
229
- | Flash of the end state | `from` outside a layout effect |
230
- | Two components animate each other | missing `scope`, or a shared hardcoded id |
231
- | Hydration mismatch | storage or media query read during render |
232
- | Janky pointer effect | React state in the handler |
233
- | Hover sticks halfway | no `overwrite: "auto"` on the retriggered tween |
234
- | `gsap.set` lands but `gsap.to` sits mid-flight — only under test | the harness waited on a timer while rAF was throttled, so the ticker never ran. Not a lifecycle bug: wait in frames — see "Timing in milliseconds, settling in frames" in performance.md |
@@ -1,161 +0,0 @@
1
- # Three.js and React Three Fiber
2
-
3
- GSAP is the **orchestration layer**, not the renderer. Three owns the scene,
4
- the camera, and the draw loop. GSAP owns *when* values change and *how* they
5
- ease between states.
6
-
7
- Neither library comes with GSAP, and most projects have neither. Check
8
- `package.json` before writing code against them, and say so if they are not
9
- there.
10
-
11
- ## The division
12
-
13
- | GSAP | Three / R3F |
14
- |---|---|
15
- | timing, easing, sequencing | geometry, materials, lights |
16
- | camera moves between positions | camera projection, the render loop |
17
- | object transforms over time | the transforms themselves |
18
- | material property transitions | shaders |
19
- | tying it all to scroll | rendering the result |
20
-
21
- Do not rebuild a physics engine or a particle system in GSAP. Do not hand-write
22
- easing in `useFrame` when a tween would say it in one line.
23
-
24
- ## The core pattern
25
-
26
- Three objects are plain JS objects, so GSAP tweens them directly. The only thing
27
- to remember is that nested properties are their own targets:
28
-
29
- ```ts
30
- gsap.to(mesh.position, { x: 2, y: 1, duration: 1.2, ease: "power3.inOut" });
31
- gsap.to(mesh.rotation, { y: Math.PI * 2, duration: 3, ease: "none", repeat: -1 });
32
- gsap.to(mesh.material, { opacity: 0, duration: 0.6 });
33
- gsap.to(mesh.scale, { x: 1.4, y: 1.4, z: 1.4, duration: 0.5 });
34
- ```
35
-
36
- `gsap.to(mesh, { position: { x: 2 } })` does not work. Target `mesh.position`.
37
-
38
- For a material property to animate opacity, `transparent` must already be true —
39
- setting it mid-tween causes a shader recompile.
40
-
41
- ## Uniforms
42
-
43
- Shader uniforms are `{ value }` objects, which tween cleanly:
44
-
45
- ```ts
46
- gsap.to(material.uniforms.uProgress, { value: 1, duration: 1.5, ease: "power2.inOut" });
47
- ```
48
-
49
- This is the usual bridge between a GSAP timeline and a custom shader effect —
50
- one uniform, driven by the timeline, interpreted by the shader.
51
-
52
- ## R3F
53
-
54
- The renderer runs its own loop, so nothing needs invalidating on demand unless
55
- the canvas is set to `frameloop="demand"` — in which case call `invalidate()`
56
- from `onUpdate`.
57
-
58
- ```tsx
59
- "use client";
60
-
61
- import { useGSAP } from "@gsap/react";
62
- import { useFrame, useThree } from "@react-three/fiber";
63
- import gsap from "gsap";
64
- import { useRef } from "react";
65
- import type { Mesh } from "three";
66
-
67
- export function Box() {
68
- const mesh = useRef<Mesh>(null);
69
-
70
- useGSAP(() => {
71
- if (!mesh.current) return;
72
- gsap.to(mesh.current.rotation, {
73
- y: Math.PI * 2,
74
- duration: 6,
75
- ease: "none",
76
- repeat: -1,
77
- });
78
- });
79
-
80
- return (
81
- <mesh ref={mesh}>
82
- <boxGeometry />
83
- <meshStandardMaterial />
84
- </mesh>
85
- );
86
- }
87
- ```
88
-
89
- **Never drive a 3D transform through React state.** `useState` per frame
90
- re-renders the React tree for a value Three reads directly. Refs and GSAP only.
91
-
92
- `useFrame` and GSAP are complementary, not competing: `useFrame` for continuous
93
- per-frame logic that depends on the clock, GSAP for transitions between states.
94
-
95
- ## Camera
96
-
97
- Camera moves are where GSAP earns its place — easing a camera by hand is how
98
- scroll-driven 3D ends up feeling mechanical.
99
-
100
- ```ts
101
- const { camera } = useThree();
102
-
103
- gsap.timeline({ defaults: { ease: "power2.inOut", duration: 2 } })
104
- .to(camera.position, { x: 0, y: 1.5, z: 4 })
105
- .to(camera.rotation, { x: -0.2 }, "<");
106
- ```
107
-
108
- If the camera uses `lookAt`, tween a proxy target and call `lookAt` in
109
- `onUpdate` — otherwise the rotation you tween is overwritten every frame:
110
-
111
- ```ts
112
- const look = { x: 0, y: 0, z: 0 };
113
- gsap.to(look, {
114
- x: 2,
115
- duration: 2,
116
- onUpdate: () => camera.lookAt(look.x, look.y, look.z),
117
- });
118
- ```
119
-
120
- ## Scroll-driven 3D
121
-
122
- Scrub a timeline, do not compute from `scrollY` in `useFrame`:
123
-
124
- ```ts
125
- gsap.timeline({
126
- scrollTrigger: { trigger: section, start: "top top", end: "+=2000",
127
- pin: true, scrub: 1 },
128
- })
129
- .to(camera.position, { z: 3, ease: "none" })
130
- .to(material.uniforms.uProgress, { value: 1, ease: "none" }, "<");
131
- ```
132
-
133
- `ease: "none"` on every child, as with any scrub. See
134
- [scrolltrigger.md](scrolltrigger.md).
135
-
136
- ## Cost and accessibility
137
-
138
- 3D is the most expensive thing on a page, and the constraints are harder than
139
- for DOM animation:
140
-
141
- - A WebGL canvas runs the GPU continuously. On a phone that is battery and heat.
142
- - Give the canvas a real fallback — a static image — for low-power devices and
143
- for `prefers-reduced-motion`. Do not merely stop the animation and leave a
144
- running canvas.
145
- - Dynamically import the 3D component so Three is not in the initial bundle.
146
- - `frameloop="demand"` when the scene is static between interactions.
147
- - Dispose geometries, materials and textures on unmount; GSAP's cleanup does not
148
- do that.
149
- - Nothing essential — text, navigation, product information — should exist only
150
- inside the canvas.
151
-
152
- ## Cleanup
153
-
154
- ```ts
155
- useGSAP(() => {
156
- const tl = gsap.timeline({ scrollTrigger: { ... } });
157
- return () => { tl.scrollTrigger?.kill(); };
158
- });
159
- ```
160
-
161
- Plus Three's own disposal, which is separate and yours to do.