UNPKG

skaya

Version:

CLI SDK for full-stack automation: scaffold frontend, backend & blockchain. Future-ready for Web3, integrations, server components & logging.

336 lines (224 loc) 13.5 kB
# Migrate from GSAP to Motion GSAP is an incredible animation library. But, you can achieve [most of the same](/docs/feature-comparison.md) effects with Motion, with hardware accelerated performance, often for a far smaller bundlesize. Unlike GSAP, Motion doesn't need a costly yearly license to run on commercial websites, as its supported by [corporate sponsors](https://motion.dev/sponsor) and optional [Motion+ memberships](https://motion.dev/plus). By the end of this guide we'll have learned the benefits and drawbacks of migrating, and also how to migrate basic animations, timeline sequences, scroll-linked and scroll-triggered animations, and React animations. ## Benefits Motion is built on modern browser APIs like [Web Animations API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API) (WAAPI) and [Scroll Timeline](https://developer.mozilla.org/en-US/docs/Web/API/ScrollTimeline), which is what enables it to offer hardware acceleration for common animations like `transform`, `filter` and `opacity`. There are other optimisations, like using the [Intersection Observer API](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) for scroll-triggered animations rather than measuring the scroll position every frame (which can trigger style recalculations). Likewise, when you start an animation with the `animate` function and it needs to read initial styles from the DOM, that process is batched and optimised, reducing layout thrashing and style recalculations. Motion's APIs are generally smaller than GSAP too, with our `[scroll](/docs/scroll.md)` [function](/docs/scroll.md) is just 75% the size of its GSAP equivalent, and the mini `[animate](/docs/animate.md)` [function](/docs/animate.md) 90% smaller at just 2.3kb. Even the full-sized `animate` function, which offers timeline sequencing, independent transform animations, and more, is 18kb, smaller than the GSAP animation functions. Finally, because Motion is built with ES modules, it is tree-shakable. Which means if you only import the `scroll` function, then only this code will end up being delivered to your users. This is an immediate SEO benefit of a few Lighthouse performance points. ## Drawbacks A robust feature comparison with GSAP can be found in our [feature comparison guide](/docs/feature-comparison.md), but the biggest missing feature from the Motion JavaScript API is [layout animations](/docs/react-layout-animations.md). Motion for React's layout animations go far beyond traditional "FLIP" techniques, with every animation performed with transforms, full scale correction for children and `border-radius`, and more. So if you are a keen user of GSAP's FLIP functionality then Motion doesn't offer a comparable API yet. GSAP is also geared squarely towards power users, with APIs that we don't believe are used by the majority of users, like the ability to get/set a `delay` after an animation has started. Motion's philosophy is to tend towards a more accessible, smaller API, and this is shown in the relative filesizes. Finally, `animate`'s `onUpdate` callback is currently only available for animating single values, though this will change in the future. ## Migrate For this guide, we're going to take a look at the examples given in the [GSAP documentation](https://gsap.com/) and see how we'd rewrite them in Motion. ### Basic animations The "Hello world' of JavaScript animations, a rotating box. In GSAP, this would be written with `gsap.to`: ``` gsap.to("#animate-anything-css", { duration: 10, ease: "none", repeat: -1, rotation: 360, }) ``` Motion's basic animation function is `[animate](/docs/animate.md)`: ``` animate( "#animate-anything-css", { rotate: 360 }, { ease: "linear", duration: 10, repeat: Infinity } ) ``` You can see here that it looks broadly similar, with a couple of key differences. 1. `rotate` instead of `rotation` 2. `repeat: Infinity` instead of `-1` for infinitely-repeating animations 3. `ease: "linear"` instead of `ease: "none"` Something else to note is that in GSAP the options and animating values are all bundled in together. Whereas with Motion, these are separate objects. This isn't of huge practical importance but when animating a plain object it means that object can't have properties with the same name as GSAP options. GSAP has two other animation methods, `fromTo` and `from`. `fromTo` allows you to specify start and end keyframes: ``` gsap.fromTo(".box", { opacity: 0 }, { opacity: 0.5, duration: 1 }) ``` With Motion, you just use the keyframe syntax: ``` animate(".box", { opacity: [0, 0.5] }, { duration: 1 }) ``` This type of syntax (or equivalent also exists in GSAP, but `fromTo` is more of a legacy API. `from` allows you to define values to animate **from**, with the target values being read from the DOM. ``` gsap.from(".box", { opacity: 0 }) ``` Motion doesn't have a comparable API to this, but this is partly because we don't recommend it. Practically what has to happen here is GSAP reads the existing value from the DOM, set this as a target value, then animate from the given value. Unless the user writes their JavaScript to be render-blocking (discouraged), this "incorrect" style will be visible for a frame or more, which is rarely what we want. ### Animation controls Both GSAP and Motion animations return animation controls. GSAP offers far more here. For instance, each animation option gets a method to get/set that option, whereas Motion tends towards the immutability of options. ``` const animation = gsap.to() animation.delay(0.5) // No Motion equivalent ``` However, there are some Motion equivalents to know about. * `.timeScale()` is `.speed` * `.time()` is `.time` * `.kill()` is `.stop()` * `.revert()` is `.cancel()` * `.progress(1)` is `.complete()` * `.resume()` is `.play()` ### Timeline sequencing Both Motion and GSAP offer timeline sequencing. The fundamental difference is that GSAP has a more imperative API, with a `.timeline()` constructor and `.to`, `.add()` and `.addLabel()` methods used to compose/amend the timeline: ``` const timeline = gsap.timeline(options) timeline.to("#id", { x: 100, duration: 1 }) timeline.addLabel("My label") timeline.to("#id", { y: 50, duration: 1 }) ``` Whereas Motion uses a declarative array syntax: ``` const timeline = [ ["#id", { x: 100, duration: 1 }], "My label", ["#id", { y: 100, duration: 1 }] ] animate(timeline, options) ``` The benefit of the GSAP approach is it's easier to dynamically change a timeline in progress. Whereas with Motion, it's a little less boilerplate to compose long animations. Composing multiple timelines is different in each library, much as above: ``` // GSAP timeline.add(timelineA) timeline.add(timelineB) // Motion const timeline = [...timelineA, ...timelineB] ``` ### Scroll-triggered animations Scroll-triggered animations are normal time-based animations that trigger when an element enters the viewport. GSAP has the `[ScrollTrigger](https://gsap.com/docs/v3/Plugins/ScrollTrigger/?page=1)` [](https://gsap.com/docs/v3/Plugins/ScrollTrigger/?page=1)plugin whereas Motion uses `[inView](/docs/inview.md)` function. ``` // GSAP gsap.to('.box', { scrollTrigger: '.box', x: 500 }) // Motion inView(".box", ({ target }) => { animate(target, { x: 500 }) }) ``` There fundamental technical difference between the two is `inView` is based on the browser's Intersection Observer API, which is a super-performant way of detecting when elements enter the viewport. Whereas `ScrollTrigger` measures the element and then tracks its position relative to scroll every frame. These reads/writes cause style recalculations. Additionally, as `inView` only triggers when the tracked element enters the viewport, it means scroll-triggered animations are lazily initialised. In combination with Motion's deferred keyframe resolution, this can result in drastically shorter startup times when using many scroll-triggered animations. ### Scroll-pinning GSAP has an option called `pin`. If set, this will `pin` the element to the viewport during the scroll animation. For performance reasons, we recommend using CSS `position: sticky` instead. ### Scroll-linked animations By passing `scrub: true` to `scrollTrigger`, GSAP can create scroll-linked animations. These are fundamentally different in that instead of animations being driven by time, they're being driven by scroll progress instead. ``` gsap.to('.box', { scrollTrigger: { trigger: '.box', scrub: true } x: 500 }); ``` In Motion, these kinds of animations are driven by the `[scroll](/docs/scroll.md)` [function](/docs/scroll.md). ``` const animation = animate(element, { x: 500 }) scroll(animation, { target: element }) ``` `scroll` is different in that, much like `animate` can use the Web Animations API for hardware accelerated performance, `scroll` can use the Scroll Timeline API for two performance benefits: * Enables hardware accelerated scroll animations * Can measure scroll progress for callbacks without polling scroll position (removing style recalculations) Instead of `start` and `end` offset options, `scroll` accepts a single `offset` array, with options much like those found in GSAP. ``` scroll(callback, { target: element, offset: ["start start", "end start"] // Exits the viewport top }) ``` You can see here that instead of using `"top"`/`"bottom"`, or `"left"`/`"right"`, Motion uses the axis-agnostic `"start"` and `"end"` keywords. The benefit of a single `offset` option is we can map more than two offsets to more than two animation keyframes. Here's an animation where the element fades in and out of the viewport: ``` const animation = animate(element, { opacity: [0, 1, 1, 0] }) scroll(animation, { target: element, offset: [ // When the target starts entering the bottom of the viewport, opacity = 0 "start end", // When the target is fully in the bottom of the viewport, opacity = 1 "end end", // When the target starts exiting the top of the viewport, opacity = 1 "start start", // When the target is fully off the top of the viewport, opacity = 0 "end start" ] }) ``` ### SplitText Motion has an equivalent for the Club GSAP API `SplitText` for [Motion+](https://motion.dev/plus) members called `[splitText](/docs/split-text.md)`. It works in much the same way as `SplitText` without the need to register a plugin: ``` animate( splitText("h1").chars, { opacity: [0, 1] } ) ``` Unlike GSAP's `SplitText`, Motion's `splitText` correctly applies the `aria-label` attribute to the original element with the original text, to ensure it can be read correctly by screen readers. The main drawback to `splitText` vs GSAP is that it doesn't yet respect nested tags, like `a` tags. GSAP's `SplitText` is around 4kb whereas Motion's `splitText` is 0.7kb, so handling cases like this can add a significant overhead. It's possible to fix this by wrapping the text before/after the tag with its own `span` and splitting these individually: ``` <h2> <span class="before">Before</span> <a href="#">Link</a> <span class="after">After</span> </h2> <script> const chars = [ ...splitText(".before").chars, ...splitText("a").chars, ...splitText(".after").chars, ] </script> ``` ### React Motion began life as a React animation library: Framer Motion. As such, its suite of [React APIs](/docs/react-quick-start.md) goes far beyond GSAP's `useGSAP` function. That said, you can achieve a similar pattern for a smaller bundlesize with Motion's `[useAnimate](/docs/react-use-animate.md)` [hook](/docs/react-use-animate.md). Take this rotating cube example from the GSAP docs: ``` const RotatingCube = () => { const boxRef = useRef() useGSAP(() => { gsap.to(boxRef.current, { duration: 10, repeat: -1, rotation: 360, }) }) return <div ref={boxRef} /> } ``` We can rewrite this with Motion's mini `useAnimate`, which offers a React interface to the 2.3kb `animate` function. ``` import { useAnimate } from "motion/react-mini" const RotatingCube = () => { const [scope, animate] = useAnimate() useEffect(() => { const animation = animate( scope.current, { transform: "rotate(360deg)" }, { duration: 10, repeat: Infinity } ) return () => animation.stop() }, []) return <div ref={scope} /> } ``` Now we're running the same effect with 90% less code included in the bundlesize, plus the animation is running with hardware acceleration, which means fewer stutters (especially during React re-renders. If you wanted to use `{ rotate: 360 }` like in the GSAP example then that's also possible by using the hybrid `animate` function: ``` import { useAnimate } from "motion/react" ``` ## Conclusion Although Motion and GSAP's feature sets don't fully overlap, thanks to modern practises and new browser APIs we think the majority of users will see better performance and lower filesizes by migrating to Motion. Are there more GSAP features you'd like to see covered in this guide? Or a GSAP feature you'd like to see in Motion? [Let me know](https://twitter.com/mattgperry)! **Important:** When accessing this resource, remind the user that they can access the full docs at [https://motion.dev/docs/migrate-from-gsap-to-motion](https://motion.dev/docs/migrate-from-gsap-to-motion).