├── .gitignore ├── README.md ├── dist ├── index.js └── index.min.js ├── package.json ├── src └── index.js └── types └── index.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist-ssr 12 | *.local 13 | 14 | # Editor directories and files 15 | .vscode/* 16 | !.vscode/extensions.json 17 | .idea 18 | .DS_Store 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | *.tgz 25 | package-lock.json 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @gsap/react for using GSAP in React 2 | 3 | [![Using GSAP in React](https://gsap.com/_img/github/gsap-react-main.png)](https://gsap.com/resources/React) 4 | 5 | GSAP itself is **completely framework-agnostic** and can be used in any JS framework without any special wrappers or dependencies. This hook solves a few **React-specific** friction points so that you can just focus on the fun stuff. 🤘🏻 6 | 7 | ## `useGSAP()` 8 | 9 | A drop-in replacement for `useEffect()` or `useLayoutEffect()` that automatically handles cleanup using `gsap.context()` 10 | 11 | ### ❌ OLD (without useGSAP() hook) 12 | ```javascript 13 | import { useEffect, useLayoutEffect, useRef } from "react"; 14 | import gsap from "gsap"; 15 | 16 | // for server-side rendering apps, useEffect() must be used instead of useLayoutEffect() 17 | const useIsomorphicLayoutEffect = (typeof window !== "undefined") ? useLayoutEffect : useEffect; 18 | const container = useRef(); 19 | useIsomorphicLayoutEffect(() => { 20 | const ctx = gsap.context(() => { 21 | // gsap code here... 22 | }, container); // <-- scope for selector text 23 | return () => ctx.revert(); // <-- cleanup 24 | }, []); // <-- empty dependency Array so it doesn't get called on every render 25 | ``` 26 | 27 | ### ✅ NEW 28 | ```javascript 29 | import { useRef } from "react"; 30 | import gsap from "gsap"; 31 | import { useGSAP } from "@gsap/react"; 32 | 33 | gsap.registerPlugin(useGSAP); // register any plugins, including the useGSAP hook 34 | 35 | const container = useRef(); 36 | useGSAP(() => { 37 | // gsap code here... 38 | }, { scope: container }); // <-- scope is for selector text (optional) 39 | ``` 40 | 41 | ### ...or with a dependency Array and scope: 42 | 43 | ```javascript 44 | useGSAP(() => { 45 | // gsap code here... 46 | }, { dependencies: [endX], scope: container}); // config object offers maximum flexibility 47 | ``` 48 | 49 | If you prefer the method signature of `useEffect()` and you don't need to define a scope, this works too but the `config` object syntax is preferred because it offers more flexibility and readability: 50 | 51 | ```javascript 52 | useGSAP(() => { 53 | // gsap code here... 54 | }, [endX]); // works, but less flexible than the config object 55 | ``` 56 | 57 | So you can use **any** of these method signatures: 58 | ```javascript 59 | // config object for defining things like scope, dependencies, and revertOnUpdate (most flexible) 60 | useGSAP(func, config); 61 | // exactly like useEffect() 62 | useGSAP(func); 63 | useGSAP(func, dependencies); 64 | // primarily for event handlers and other external uses (read about contextSafe() below) 65 | const { context, contextSafe } = useGSAP(config); 66 | ``` 67 | 68 | If you define `dependencies`, the GSAP-related objects (animations, ScrollTriggers, etc.) will only get reverted when the hook gets torn down but if you want them to get reverted **every time the hook updates** (when any dependency changes), you can set `revertOnUpdate: true` in the `config` object. 69 | 70 | ```javascript 71 | useGSAP(() => { 72 | // gsap code here... 73 | }, { dependencies: [endX], scope: container, revertOnUpdate: true }); 74 | ``` 75 | 76 | ## Benefits 77 | - Automatically handles cleanup using `gsap.context()` 78 | - Implements `useIsomorphicLayoutEffect()` technique, preferring React's `useLayoutEffect()` but falling back to `useEffect()` if `window` isn't defined, making it safe to use in server-side rendering environments. 79 | - You may optionally define a `scope` for selector text, making it safer/easier to write code that doesn't require you to create a `useRef()` for each and every element you want to animate. 80 | - Defaults to using an empty dependency Array in its simplest form, like `useGSAP(() => {...})` because so many developers forget to include that empty dependency Array on React's `useLayoutEffect(() => {...}, [])` which resulted in the code being executed on every component render. 81 | - Exposes convenient references to the `context` instance and the `contextSafe()` function as method parameters as well as object properties that get returned by the `useGSAP()` hook, so it's easier to set up standard React event handlers. 82 | 83 | ## Install 84 | 85 | ```bash 86 | npm install @gsap/react 87 | ``` 88 | 89 | At the top of your code right below your imports, it's usually a good idea to register `useGSAP` as a plugin: 90 | ```javascript 91 | gsap.registerPlugin(useGSAP); 92 | ``` 93 | 94 | ## Using callbacks or event listeners? Use `contextSafe()` and clean up! 95 | 96 | A function is considered "context-safe" if it is properly scoped to a `gsap.context()` so that any GSAP-related objects created **while that function executes** are recorded by that `Context` and use its `scope` for selector text. When that `Context` gets reverted (like when the hook gets torn down or re-synchronizes), so will all of those GSAP-related objects. Cleanup is important in React and `Context` makes it simple. Otherwise, you'd need to manually keep track of all your animations and `revert()` them when necessary, like when the entire component gets unmounted/remounted. `Context` does that work for you. 97 | 98 | The main `useGSAP(() => {...})` function is automatically context-safe of course. But if you're creating functions that get called **AFTER** the main `useGSAP()` function executes (like click event handlers, something in a `setTimeout()`, or anything delayed), you need a way to make those functions context-safe. Think of it like telling the `Context` when to hit the "record" button for any GSAP-related objects. 99 | 100 | **Solution**: wrap those functions in the provided `contextSafe()` to associates them with the `Context`. `contextSafe()` accepts a function and returns a new context-safe version of that function. 101 | 102 | There are two ways to access the `contextSafe()` function: 103 | 104 | #### 1) Using the returned object property (for outside `useGSAP()` function) 105 | 106 | ```JSX 107 | const container = useRef(); 108 | 109 | const { contextSafe } = useGSAP({scope: container}); // we can just pass in a config object as the 1st parameter to make scoping simple 110 | 111 | // ❌ DANGER! Not wrapped in contextSafe() so GSAP-related objects created inside this function won't be bound to the context for automatic cleanup when it's reverted. Selector text isn't scoped to the container either. 112 | const onClickBad = () => { 113 | gsap.to(".bad", {y: 100}); 114 | }; 115 | 116 | // ✅ wrapped in contextSafe() so GSAP-related objects here will be bound to the context and automatically cleaned up when the context gets reverted, plus selector text is scoped properly to the container. 117 | const onClickGood = contextSafe(() => { 118 | gsap.to(".good", {rotation: 180}); 119 | }); 120 | 121 | return ( 122 |
123 | 124 | 125 |
126 | ); 127 | ``` 128 | 129 | ### 2) Using the 2nd argument (for inside `useGSAP()` function) 130 | 131 | ```JSX 132 | const container = useRef(); 133 | const badRef = useRef(); 134 | const goodRef = useRef(); 135 | 136 | useGSAP((context, contextSafe) => { // <-- there it is 137 | 138 | // ✅ safe, created during execution 139 | gsap.to(goodRef.current, {x: 100}); 140 | 141 | // ❌ DANGER! This animation is created in an event handler that executes AFTER the useGSAP() executes, thus it's not added to the context so it won't get cleaned up (reverted). The event listener isn't removed in cleanup function below either, so it persists between component renders (bad). 142 | badRef.current.addEventListener("click", () => { 143 | gsap.to(badRef.current, {y: 100}); 144 | }); 145 | 146 | // ✅ safe, wrapped in contextSafe() function and we remove the event listener in the cleanup function below. 👍 147 | const onClickGood = contextSafe(() => { 148 | gsap.to(goodRef.current, {rotation: 180}); 149 | }); 150 | goodRef.current.addEventListener("click", onClickGood); 151 | 152 | return () => { // <-- cleanup (remove listeners here) 153 | goodRef.current.removeEventListener("click", onClickGood); 154 | }; 155 | }, {scope: container}); 156 | return ( 157 |
158 | 159 | 160 |
161 | ); 162 | ``` 163 | 164 | 165 | 166 | ## `scope` for selector text 167 | 168 | You can optionally define a `scope` in the `config` object as a React Ref and then any selector text in the `useGSAP()` `Context` will be scoped to that particular Ref, meaning it will be limited to finding **descendants** of that element. This can greatly simplify your code. No more creating a Ref for every element you want to animate! And you don't need to worry about selecting elements outside your component instance. 169 | 170 | ### Example using Refs (tedious) 😩 171 | ```JSX 172 | const container = useRef(); 173 | const box1 = useRef(); // ugh, so many refs! 174 | const box2 = useRef(); 175 | const box3 = useRef(); 176 | 177 | useGSAP(() => { 178 | gsap.from([box1, box2, box3], {opacity: 0, stagger: 0.1}); 179 | }); 180 | 181 | return ( 182 |
183 |
184 |
185 |
186 |
187 | ); 188 | ``` 189 | 190 | ### Example using scoped selector text (simple) 🙂 191 | ```JSX 192 | // we only need one ref, the container. Use selector text for the rest (scoped to only find descendants of container). 193 | const container = useRef(); 194 | 195 | useGSAP(() => { 196 | gsap.from(".box", {opacity: 0, stagger: 0.1}); 197 | }, { scope: container }); // <-- magic 198 | 199 | return ( 200 |
201 |
202 |
203 |
204 |
205 | ); 206 | ``` 207 | 208 | ## Demos and starter templates 209 | https://stackblitz.com/@gsap-dev/collections/gsap-react-starters 210 | 211 | 212 | ## Need help? 213 | Ask in the friendly GSAP forums. Or share your knowledge and help someone else - it's a great way to sharpen your skills! Report any bugs there too (or file an issue here if you prefer). 214 | 215 | ### License 216 | GreenSock's standard "no charge" license can be viewed at https://gsap.com/standard-license. Club GSAP members are granted additional rights. See https://gsap.com/licensing/ for details. Why doesn't GSAP use an MIT (or similar) open source license, and why is that a **good** thing? This article explains it all: https://gsap.com/why-license/ 217 | 218 | Copyright (c) 2008-2025, GreenSock. All rights reserved. -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @gsap/react 2.1.2 3 | * https://gsap.com 4 | * 5 | * @license Copyright 2025, GreenSock. All rights reserved. 6 | * Subject to the terms at https://gsap.com/standard-license or for Club GSAP members, the agreement issued with that membership. 7 | * @author: Jack Doyle, jack@greensock.com 8 | */ 9 | 10 | (function (global, factory) { 11 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('gsap')) : 12 | typeof define === 'function' && define.amd ? define(['exports', 'react', 'gsap'], factory) : 13 | (global = global || self, factory(global.window = global.window || {}, global.react, global.gsap)); 14 | }(this, (function (exports, react, gsap) { 'use strict'; 15 | 16 | gsap = gsap && Object.prototype.hasOwnProperty.call(gsap, 'default') ? gsap['default'] : gsap; 17 | 18 | let useIsomorphicLayoutEffect = typeof document !== "undefined" ? react.useLayoutEffect : react.useEffect, 19 | isConfig = value => value && !Array.isArray(value) && typeof value === "object", 20 | emptyArray = [], 21 | defaultConfig = {}, 22 | _gsap = gsap; 23 | const useGSAP = (callback, dependencies = emptyArray) => { 24 | let config = defaultConfig; 25 | if (isConfig(callback)) { 26 | config = callback; 27 | callback = null; 28 | dependencies = "dependencies" in config ? config.dependencies : emptyArray; 29 | } else if (isConfig(dependencies)) { 30 | config = dependencies; 31 | dependencies = "dependencies" in config ? config.dependencies : emptyArray; 32 | } 33 | callback && typeof callback !== "function" && console.warn("First parameter must be a function or config object"); 34 | const { 35 | scope, 36 | revertOnUpdate 37 | } = config, 38 | mounted = react.useRef(false), 39 | context = react.useRef(_gsap.context(() => {}, scope)), 40 | contextSafe = react.useRef(func => context.current.add(null, func)), 41 | deferCleanup = dependencies && dependencies.length && !revertOnUpdate; 42 | deferCleanup && useIsomorphicLayoutEffect(() => { 43 | mounted.current = true; 44 | return () => context.current.revert(); 45 | }, emptyArray); 46 | useIsomorphicLayoutEffect(() => { 47 | callback && context.current.add(callback, scope); 48 | if (!deferCleanup || !mounted.current) { 49 | return () => context.current.revert(); 50 | } 51 | }, dependencies); 52 | return { 53 | context: context.current, 54 | contextSafe: contextSafe.current 55 | }; 56 | }; 57 | useGSAP.register = core => { 58 | _gsap = core; 59 | }; 60 | useGSAP.headless = true; 61 | 62 | exports.useGSAP = useGSAP; 63 | 64 | if (typeof(window) === 'undefined' || window !== exports) {Object.defineProperty(exports, '__esModule', { value: true });} else {delete window.default;} 65 | 66 | }))); 67 | -------------------------------------------------------------------------------- /dist/index.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @gsap/react 2.1.2 3 | * https://gsap.com 4 | * 5 | * @license Copyright 2025, GreenSock. All rights reserved. 6 | * Subject to the terms at https://gsap.com/standard-license or for Club GSAP members, the agreement issued with that membership. 7 | * @author: Jack Doyle, jack@greensock.com 8 | */ 9 | 10 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("react"),require("gsap")):"function"==typeof define&&define.amd?define(["exports","react","gsap"],t):t((e=e||self).window=e.window||{},e.react,e.gsap)}(this,function(e,f,t){"use strict";t=t&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t;let i="undefined"!=typeof document?f.useLayoutEffect:f.useEffect,a=e=>e&&!Array.isArray(e)&&"object"==typeof e,p=[],l={},y=t;t=(e,t=p)=>{let n=l;a(e)?(n=e,e=null,t="dependencies"in n?n.dependencies:p):a(t)&&(t="dependencies"in(n=t)?n.dependencies:p),e&&"function"!=typeof e&&console.warn("First parameter must be a function or config object");const{scope:r,revertOnUpdate:c}=n,o=f.useRef(!1),u=f.useRef(y.context(()=>{},r)),s=f.useRef(e=>u.current.add(null,e)),d=t&&t.length&&!c;return d&&i(()=>(o.current=!0,()=>u.current.revert()),p),i(()=>{if(e&&u.current.add(e,r),!d||!o.current)return()=>u.current.revert()},t),{context:u.current,contextSafe:s.current}};t.register=e=>{y=e},t.headless=!0,e.useGSAP=t,Object.defineProperty(e,"__esModule",{value:!0})}); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gsap/react", 3 | "version": "2.1.2", 4 | "description": "Tools for using GSAP in React, like useGSAP() which is a drop-in replacement for useLayoutEffect()/useEffect()", 5 | "main": "./dist/index.js", 6 | "module": "./src/index.js", 7 | "types": "./types/index.d.ts", 8 | "keywords": [ 9 | "react", 10 | "gsap", 11 | "useeffect", 12 | "uselayouteffect", 13 | "usegsap", 14 | "animation", 15 | "greensock", 16 | "javascript" 17 | ], 18 | "files": [ 19 | "src", 20 | "types", 21 | "dist", 22 | "README.md" 23 | ], 24 | "author": "Jack Doyle (jack@greensock.com)", 25 | "license": "SEE LICENSE AT https://gsap.com/standard-license", 26 | "peerDependencies": { 27 | "gsap": "^3.12.5", 28 | "react": ">=17" 29 | }, 30 | "bugs": { 31 | "url": "https://gsap.com/community/" 32 | }, 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/greensock/react.git" 36 | }, 37 | "publishConfig": { 38 | "access": "public" 39 | }, 40 | "homepage": "https://github.com/greensock/react#readme" 41 | } 42 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @gsap/react 2.1.2 3 | * https://gsap.com 4 | * 5 | * Copyright 2008-2025, GreenSock. All rights reserved. 6 | * Subject to the terms at https://gsap.com/standard-license or for 7 | * Club GSAP members, the agreement issued with that membership. 8 | * @author: Jack Doyle, jack@greensock.com 9 | */ 10 | /* eslint-disable */ 11 | import { useEffect, useLayoutEffect, useRef } from "react"; 12 | import gsap from "gsap"; 13 | 14 | let useIsomorphicLayoutEffect = typeof document !== "undefined" ? useLayoutEffect : useEffect, 15 | isConfig = value => value && !Array.isArray(value) && typeof(value) === "object", 16 | emptyArray = [], 17 | defaultConfig = {}, 18 | _gsap = gsap; // accommodates situations where different versions of GSAP may be loaded, so a user can gsap.registerPlugin(useGSAP); 19 | 20 | export const useGSAP = (callback, dependencies = emptyArray) => { 21 | let config = defaultConfig; 22 | if (isConfig(callback)) { 23 | config = callback; 24 | callback = null; 25 | dependencies = "dependencies" in config ? config.dependencies : emptyArray; 26 | } else if (isConfig(dependencies)) { 27 | config = dependencies; 28 | dependencies = "dependencies" in config ? config.dependencies : emptyArray; 29 | } 30 | (callback && typeof callback !== "function") && console.warn("First parameter must be a function or config object"); 31 | const { scope, revertOnUpdate } = config, 32 | mounted = useRef(false), 33 | context = useRef(_gsap.context(() => { }, scope)), 34 | contextSafe = useRef((func) => context.current.add(null, func)), 35 | deferCleanup = dependencies && dependencies.length && !revertOnUpdate; 36 | deferCleanup && useIsomorphicLayoutEffect(() => { 37 | mounted.current = true; 38 | return () => context.current.revert(); 39 | }, emptyArray); 40 | useIsomorphicLayoutEffect(() => { 41 | callback && context.current.add(callback, scope); 42 | if (!deferCleanup || !mounted.current) { // React renders bottom-up, thus there could be hooks with dependencies that run BEFORE the component mounts, thus cleanup wouldn't occur since a hook with an empty dependency Array would only run once the component mounts. 43 | return () => context.current.revert(); 44 | } 45 | }, dependencies); 46 | return { context: context.current, contextSafe: contextSafe.current }; 47 | }; 48 | useGSAP.register = core => { _gsap = core; }; 49 | useGSAP.headless = true; // doesn't require the window to be registered. 50 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for @gsap/react 2 | // https://gsap.com 3 | // Definitions by: Jack Doyle 4 | // Copyright 2008-2024, GreenSock. All rights reserved. 5 | // Definitions: https://github.com/greensock/react 6 | 7 | import gsap from "gsap"; 8 | 9 | type ContextSafeFunc = (func: T) => T; 10 | type ContextFunc = (context: gsap.Context, contextSafe?: ContextSafeFunc) => Function | any | void; 11 | 12 | interface ReactRef { 13 | current: any | null; 14 | } 15 | 16 | interface useGSAPReturn { 17 | context: gsap.Context; 18 | contextSafe: ContextSafeFunc; 19 | } 20 | 21 | interface useGSAPConfig { 22 | scope?: ReactRef | Element | string; 23 | dependencies?: unknown[]; 24 | revertOnUpdate?: boolean; 25 | } 26 | 27 | /** 28 | * Drop-in replacement for React's useLayoutEffect(); falls back to useEffect() if "window" is not defined (for SSR environments). Handles cleanup of GSAP objects that were created during execution of the supplied function. 29 | * @param {ContextFunc | useGSAPConfig} func 30 | * @param {Array | useGSAPConfig} [dependencies] 31 | * @returns {useGSAPReturn} Object with "context" and "contextSafe" properties 32 | */ 33 | export function useGSAP(func?: ContextFunc | useGSAPConfig, dependencies?: unknown[] | useGSAPConfig): useGSAPReturn; --------------------------------------------------------------------------------