Documentation

Documentation

JavaScript
Migrate from GSAP to Motion

Migrate from GSAP to Motion

GSAP is an incredible animation library. But, you can achieve most of the same effects with Motion, with hardware accelerated performance, often for a far smaller bundlesize.

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 (WAAPI) and Scroll Timeline, 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 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 function is just 75% the size of its GSAP equivalent, and the mini animate function 90% smaller. Even the hybrid 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, but the biggest missing feature from the Motion JavaScript API is layout animations.

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 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:

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 plugin whereas Motion uses inView 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 function.

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 end"
  ]
})

React

Motion began life as a React animation library: Framer Motion. As such, its suite of React APIs goes far beyond GSAP's useGSAP function.

That said, you can achieve a similar pattern for a smaller bundlesize with Motion's useAnimate hook.

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.5kb 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!

A Motion+ membership will give you early access to features & content, access to our private Discord, and more.

It's a one-time fee that grants lifetime access. No subscription necessary!