Should you use Framer Motion or Motion One?

Photo of Matt Perry
Matt Perry / June 2022

Hello everyone. My name's Matt Perry and I clearly have an addiction to maintaining JavaScript animation libraries.

Motion One is a tiny library built atop the browser's native Web Animations API (WAAPI). It aims to push the limits of WAAPI while also providing a nicer API in as small a package as possible.

The other, Framer Motion is a declarative, batteries-included animation library. It's predominantly for React but also offers functions that work in all JavaScript environments. It also provides WAAPI animations for great performance but also uses JS animations to surpass all of WAAPI's limitations. As a result it is also much larger.

Two different libraries with two different sets of design goals. But, since the release of the latter, I've been asked many times by developers which they should use in their projects.

TLDR: If you're a React developer, you almost certainly want to use Framer Motion! Everyone else needs to think about bundlesize vs feature set.

To start, let's take a brief look at the inception and fundamental design goals of each library.

Framer Motion in a nutshell

Today, Framer is an incredible website creator with the user experience and flexibility of a design tool like Figma or Sketch. You can achieve amazing results with zero code.

However, back in 2018, Framer was a prototyping tool that allowed you to bring life to your flat designs using code. Code wasn't something you could add for a little flourish like it is today, all animations had to be created with it.

Framer's audience primarily consists of people either who don't code or don't code as their primary job, and yet the only way to animate back then was to code. Not ideal.

So, to make animation as easy as possible, the primary design philosophy of Framer Motion was, and continues to be, simplicity.

At the time, Framer had been freshly rebuilt atop React. This allowed us to write a declarative API, where animations triggered on mount and via gestures can be defined as simple descriptions:

<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
  whileHover={{ scale: 1.2 }}
/>

This API avoids the need for developers to interact with the DOM via selectors, refs or other kinds of error-prone, severable wiring.

It also abstracts much of the busyness typically associated with traditional JavaScript animation libraries and brings the experience of adding features like spring animations, mount/unmount animations, gesture animations etc, much closer to CSS.

In franker words, there is much less stuff to fuck up.

The great thing about a declarative API like this, is features are much quicker to write, easier to read, more robust to maintain, and therefore cheap to iterate or delete.

Motion One-oh-one

About a year ago I was kicking the tires of WAAPI. It's essentially a programmatic interface to many of the features offered by CSS transitions and animations, going beyond in some areas.

I had originally wanted to write an educational course on it, but I kept running into cross-browser inconsistencies, implementation bugs (predominantly in Safari), and strange design choices.

Additionally, the API falls short of many features you'd expect of a traditional JavaScript animation library like Greensock. For instance, there's no support for springs, animating transforms independently, or sequencing complex timelines.

And yet, it has some amazing potential upsides. Unlike requestAnimationFrame, the backbone of traditional JS animation, it is uniquely capable of running animations on the GPU.

Further, it can animate between value units, like px and %, rgba and hsla, or calc() and var(), for free. With JS animations this can only otherwise be achieved with (sometimes significant) bundlesize and runtime overhead.

I felt like there were enough benefits to justify a library that smoothed out WAAPI's rough edges, enhanced its capabilities, while keeping bundlesize to a minimum. So the design goal of Motion One is to be the jQuery of WAAPI.

As a result, it achieves much of the same feature set as libraries like Greensock, in some cases furthering it, but for just 15% of the kb.

Yet, being based on WAAPI means there's some things it will simply never be able to do, most notably the ability to access values every animation frame. Once they're in the WAAPI black box, they're not coming out until the animation is finished. This is a real flipside to requestAnimationFrame, where direct access to values is intrinsic.

Design goals and compromise

For every library I write, I try and understand its primary role and essence through a simple design goal.

The same way in the Python world they might say something is "pythonic" when it aligns with the nature and philosophy of Python, each new API for a given library should be created in service of the design goal.

For Framer Motion, this has been simplicity. For Motion One, the jQuery of WAAPI.

The most important use of a clear design goal is to help me decide which way to call it when I encounter a compromise.

For instance, my favourite feature of Framer Motion is its "sensible defaults".

If no transition is explicitly defined, then Framer Motion will generate one based on the type of value being animated. In broad strokes, independent transforms get some kind of spring and everything else, like opacity/color, are animated with a duration-based easing.

This means animating independent transforms with a spring easing on hover looks like this:

<motion.div animate={{ rotate: 90 }} whileTap={{ scale: 0.9 }} />

Which is simpler than even CSS. By default, this animation will feel pretty good, even if you know nothing about animation.

However, to achieve an API like this, the code to run these springs and custom easings must be included in the overall bundlesize. Many little choices like this add up.

By contrast, Motion One's design goal is to push WAAPI beyond its core capabilities, but keeping an eye on bundlesize. Everything should be small, or at least tree-shakable.

When animating independent transforms with a spring, spring must be imported explicitly. So if you never use it, users will never download it.

To achieve the same rotation animation, with a scale animation on press, the code will look roughly like this:

import { animate, spring } from "motion"

const element = document.getElementById("my-element")

animate(element, { rotate: 90 }, { easing: spring() })

element.addEventListener("pointerdown", startPointerDownAnimation)

function startPointerDownAnimation(event) {
  animate(
    event.currentTarget,
    { scale: 0.9 },
    { easing: spring({ stiffness: 300, damping: 30 }) }
  )

  window.addEventListener("pointerup", startPointerUpAnimation)
}

function startPointerUpAnimation(event) {
  animate(event.currentTarget, { scale: 1 }, { easing: spring() })

  window.removeEventListener("pointerup", startPointerUpAnimation)
}

Now, you'll be forgiven for reading this code and thinking it isn't the explicit spring import that makes the biggest difference here, and you'd be right.

In fact even this code, as verbose as it is, is really only low-res approximation of what Framer Motion does under the hood to provide things like lifecycle management, iOS 13 compatibility for pointer events, filtering non-primary pointers and annotating events with extra pointer data.

"Framer Motion or Motion One?" is a false dichotomy. Framer Motion offers both declarative, high-level APIs but also imperative, low-level ones. Whereas Motion One only offers the latter. They're different beasts, with different use cases.

Declarative vs imperative code

What do I mean by declarative vs imperative? If you've ever written HTML you might already have an intuitive sense. Look at this:

<body>
  <p>Hello world!</p>
</body>

This doesn't appear in a browser like magic, the browser takes this markup, this description, this declaration, and converts it to a series of instructions that end up drawing the page you see on screen.

Look at how we'd build this same markup in JavaScript:

const newParagraph = document.createElement("p")
newParagraph.innerText = "Hello world!"
document.body.appendChild(newParagraph)

Notice how the HTML is a description, whereas the JavaScript is a series of instructions.

The HTML can be said to be more declarative whereas the JavaScript can be said to be more imperative.

I say "more" because I don't think there's a hard and fast boundary between the two, it's more of a gradient. If a real low-level developer, who deals daily with memory allocation and CPU instructions, is reading this they would positively balk at my description of JavaScript as being imperative because so much work is implicit in a seemingly simple command like element.innerText = "".

So to answer your question

In my opinion there's nothing intrinsically better about declarative or imperative APIs, but depending on your use case one can usually be preferred.

For instance, if you're already splashing around in the world of direct DOM manipulation then an imperative library like Motion One could fit your usage enough that you don't mind having to use imperative APIs.

Whereas if you're a Vue or Solid developer than the Motion One APIs specifically built to live in those declarative worlds are going to leave your code cleaner and more robust than using the framework-specific escape hatches to tie directly into the imperative APIs.

If you're a React developer asking "Framer Motion or Motion One?" you're asking whether you want your absolute simplest animation to look like this:

return <motion.div animate={{ scale: 2 }} />

Or to look like this:

const ref = useRef(null)

useEffect(() => {
  animate(ref.current, { scale: 2 })
}, [])

return <div ref={ref} />

With the former scaling neatly as you add more functionality by sprinkling over more props and the latter getting more complicated as you try an interweave various side effects and event listeners while juggling refs.

One last caveat

For 99% of React developers I recommend the better developer experience of Framer Motion. However, things aren't always so black and white.

If you're on a strict bundlesize budget, you only use animation features, and Framer Motion's ability to lazy-load animations and tree-shake features still doesn't crank the bundlesize down far enough - then we come back to the compromise.

If these are your fundamental design goals, then the compromise is to forego the DX and features, and in that case then Motion One is a good option.

Likewise, if you're not using React, then you simply need to think about whether the extra features of Framer Motion are worth the extra bundlesize to you.