🚂 Framer-Motion: New And Underestimated Features
Tags: framer, motion, tutorial, webdev
Published: 2022-09-06 07:37:42

## Introduction
Framer-motion is a motion library that allows you to create declarative animations, layout transitions and gestures while maintaining HTML and SVG element semantics.

---
⚛️ Beware, framer-motion is React-only library.

In case it might be an issue for you, check out the greensock (framework agnostic animation library) tutorial.

---
You might be familiar with our previous article on the subject (**Framer Motion Tutorials: Make More Advanced Animations**). It first came out quite a few versions of this library ago, so in this article we want to:

- introduce previously overlooked and more recent features of framer-motion.
- remove styled-components to make demos a bit easier and to lower the entry threshold
- cover how to use framer-motion with consideration to accessibility (prefers-reduced-motion) and bundle size.

**Contents:**

- Setup
- Keyframes
- Gesture Animations
- Accessibility
- MotionConfig and reducedMotion
- useReducedMotion
- How to use prefers-reduced-motion with vanilla CSS and JavaScript
- Scroll Trigger animation (whileInView)
- Scroll Linked animations (useViewportScroll, useTransform)
- Non-custom approach
- Custom useElementViewportPosition hook
- Reduce bundle size with LazyMotion
- Synchronous loading
- Asynchronous loading
- Bundle size test
- Layout animations
- Shared layout animations
- Conclusion

## Setup
You can start with **framer-motion** in two easy steps:

Add package to your project: run _npm install framer-motion or yarn add framer-motion_.
Import _motion_ component.
Start animating with _motion_ component (rename any HTML tag with _motion_ prefix, for example _motion.div_, _motion.p_, _motion.rect_) and configure animation with very straight-forward _animate_ prop.
And ✨ **the magic** ✨ is already at your fingertips!

```js
import { motion } from 'framer-motion';
 
export const MotionBox = ({ isAnimating }: { isAnimating: boolean }) => {
 return (
  <motion.div animate={{ opacity: isAnimating ? 1 : 0 }} />;
};
```

## Keyframes

Values in _animate_ can not only take single values (check out the previous example ⬆️) but also an array of values, which is very similar to CSS keyframes.

By default [keyframes](https://www.framer.com/docs/animation/%23%2523keyframes/) animation will start with the first value of an array. Current value can be set using _null_ placeholder, to create seamless transitions in case the value was already animating.

```js
<motion.div 
 style={{ transform: 'scale(1.2)' }} 
 animate={{ scale: [null, 0.5, 1, 0] }}
/>

```
Each value in the keyframes array by default is spaced evenly throughout the animation. To overwrite it and define timing for each keyframe step pass _times_ (an array of values between 0 and 1. It should have the same length as the keyframes animation array) to the _[transition](https://www.framer.com/docs/transition/)_ prop.

```js
<motion.div
     animate={{ opacity: [0, 0.2, 0.8, 1] }}
     transition={{ ease: 'easeInOut', duration: 3, times: [0, 0.5, 0.6, 1] }}
   />
```
Let’s animate some SVGs. To create a basic rotating loader we need:

_rotate: 360 animation_ and _transform-origin: center (originX and originY)_ on root svg element.
Arrays of keyframes for CSS properties:
_[stroke-dasharray](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray)_ (pattern of dashes and gaps for outline of the shape).
_[stroke-dashoffset](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dashoffset)_ (offset on the rendering of the dash array).

```js
<motion.svg
     {...irrelevantStyleProps}
     animate={{ rotate: 360 }}
     transition={{ ease: 'linear', repeat: Infinity, duration: 4 }}
     style={{ originX: 'center', originY: 'center' }}
   >
     <circle    {...irrelevantProps} />
     <motion.circle
       {...irrelevantStyleProps}
       animate={{
         strokeDasharray: ['1, 150', '90, 150', '90, 150'],
         strokeDashoffset: [0, -35, -125],
       }}
       transition={{ ease: 'easeInOut', repeat: Infinity, duration: 2 }}
     />
   </motion.svg>
```
You can compare css keyframes with **framer-motion** keyframes animation in the demo down below.

{% embed https://codesandbox.io/embed/fr-forked-qxjbdj?fontsize=14&hidenavigation=1&theme=dark %}

I personally see no difference in the difficulty of both realizations.

It’s probably a matter of personal preference or the project’s specificity. In any case, I feel like CSS keyframes should be preferred solution for this type of animation, especially if there are no need for any complex calculations or for the performance concerns (shipping less JavaScript is usually better) and if project is not already using **framer-motion** for something else obviously.

## Gesture animations
`Motion` component allows us to animate visual representations of interactive elements with `whileHover`, `whileTap`, `whileDrag` and `whileFocus props`.

Just like with any other type of animation, we can either pass animation directly into props:
`<motion.button whileHover={{ scale: 1.2 }}>Submit</motion.button>`
Or use `variants` for more complex customisation:

```js
import { motion, Variants } from 'framer-motion';
 
const variants: Variants = { variants: { tap: { scale: 0.9 } } };
 
export const Component = () => {
 return (
   <motion.button variants={variants} whileTap="tap">
     Send
   </motion.button>
 );
};
```
{% embed https://codesandbox.io/embed/g-anim-forked-c3mq85?fontsize=14&hidenavigation=1&theme=dark %}
Similarly to `keyframes`, in case there is no need for complex calculations or transitions, most of those props (except `whileDrag`) can be easily replaced with CSS animations.

## Accessibility

## MotionConfig and reducedMotion
Animations on web pages may be used to attract attention to certain elements or to make UI/UX experience more smooth and enjoyable. But parallax effects, fade-in-out popups or basically any moving or flashing element might cause motion sickness or in any other way might be inconvenient or uncomfortable for a user.

So both for accessibility and performance reasons we need to give an option to restrict or even disable any motion on the page, and, as developers, to respect this choice and make sure it is implemented.

`prefers-reduced-motion` is a CSS media feature to detect if a user is indicated in their system setting to reduce non-essential motion.

**🔗 More information on the subject:**

**- An Introduction to the Reduced Motion Media Query**  by Eric Bailey, February 10, 2017
**- Respecting Users’ Motion Preferences**  by Michelle Barker, October 21, 2021 

To properly implement `prefers-reduced-motion` with **framer-motion**, we can use `MotionConfig` – a component that allows us to set default options to all child `motion` components.

At the present moment in only takes 2 props: `transition` and `reducedMotion`.

reducedMotion lets us set a policy for handling reduced motion:

- `user` – respect user’s device setting;
- `always` – enforce reduced motion;
- `never` – don’t reduce motion.

In case of reduced motion, `transform` and `layout` animations will be disabled and animations like `opacity `or `backgroundColor` will stay enabled.
{% embed https://codesandbox.io/embed/motions-forked-92q4b2?fontsize=14&hidenavigation=1&theme=dark %}

In this particular demo I had trouble dynamically changing  `reduceMotion` value. It wasn’t changing the inner `Context.Provider` prop  unless I passed `key={reducedMotion}`. This is a very questionable and temporary solution just to make it work, but it at the same time triggers a re-render, which is far away for a perfect and definitely not production-ready solution.

## useReducedMotion

For more custom and complex solutions or for any task outside the library’s scope (for example, to disable video autoplaying) we can use `useReducedMotion` hook:

```js
import { useReducedMotion, motion } from 'framer-motion';

export const Component = () => {
 const shouldReduceMotion = useReducedMotion();
 return(
   <motion.div
    animate={shouldReduceMotion ? { opacity: 1 } : { x: 100 }} 
    />
);
}
```

It makes me very happy, both as a user and as a developer, that **framer-motion** is taking accessibility very seriously and gives us proper tools to meet any user’s needs. Even though we have proper JavaScript and CSS solutions to do the same.

## How to use prefers-reduced-motion with vanilla CSS and JavaScript
You can also achieve the same thing by using prefers-reduced-motion media-query with CSS:

```js
@media (prefers-reduced-motion: reduce) {
 button {
   animation: none;
 }
}
 
@media (prefers-reduced-motion: no-preference) {
 button {
   /* `scale-up-and-down` keyframes are defined elsewhere */
   animation: scale-up-and-down 0.3s ease-in-out infinite both;
 }
}
```
Or `window.matchMedia()` with JavaScript:

```js
const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
 
mediaQuery.addEventListener('change', () => {
 const isPrefersReducedMotion = mediaQuery.matches;
 console.log({ isPrefersReducedMotion });
 
 if (isPrefersReducedMotion === true) {
   // disable animation
 }
});
```

## Scroll Trigger animation (whileInView)
Scroll Trigger animation is a great way to capture users attention and make elements more dynamic.

To create Scroll Trigger animation with framer-motion, let’s use `whileInView` prop.

```js
<motion.div
 initial={{ opacity: 0, y: -100 }} 
 whileInView={{ opacity: 1, y: 0 }} 
/>
```
We can also use `onViewportEnter` and `onViewportLeave` callbacks, that return `IntersectionObserverEntry`.

Let’s use `variants` ⬇️, which make it a bit easier to define more complex animations.

There is also a way to set configuration with `viewport` props, some of the options we can use:

- `once: boolean` – if true, whileInView animation triggers only once.
- `amount: ‘some’ | ‘all’ | number` – default: `‘some’`. Describes the amount that element has to intersect with viewport in order to be considered in view; `number` value can be anything from 0 to 1.

```js
import { motion, Variants } from 'framer-motion';
 
const variants: Variants = {
 hidden: { opacity: 0 },
 visible: { opacity: 1 },
 slideStart: { clipPath: 'inset(0 100% 0 0 round 8px)' },
 slideEnd: { clipPath: 'inset(0 0% 0 0 round 8px)' },
};
 
export const Component = () => {
 return (
   <motion.div
     variants={variants}
     initial={['hidden', 'slideStart']}
     whileInView={['visible', 'slideEnd']}
     exit={['hidden', 'slideStart']}
     viewport={{ amount: 0.4, once: true }}
   />
 );
};
```
{% embed https://codesandbox.io/embed/scrltran-forked-43kq7x?fontsize=14&hidenavigation=1&theme=dark %}

One unfortunate limitation is that `whileInView` doesn’t work with `transition``: { repeat: Infinity }`. So there’s no easy way to do infinitely repeating animations that play only in the users viewport. `onViewportEnter` and `onViewportLeave` callbacks (with ⚛️ **React’s** hooks `useState` and `useCallback`) is probably the best way to do it.

## Scroll Linked animations (useViewportScroll, useTransform)
To take it one step further, let’s talk about Scroll Linked animations, which are a bit similar to Scroll Triggered animations, but more fun! ✨

useViewportScroll and useTransform are animations that are bound to scrolling and scroll position. Scroll Trigger animations give users a more exciting browsing experience. It can be used to capture users’ attention, and it is a great tool for creative storytelling.

Probably the most popular pick for Scroll Linked animations is GreenSock’s ScrollTrigger (just like in our demo).

Unfortunately, compared to GreenSock, Scroll Linked animations with **framer-motion** will require more custom solutions and take more time to achieve. Let’s find out why and figure out the approach step by step.

## Non-custom approach
For Scroll Linked animations with **framer-motion**, we need:

1. `useViewportScroll` hook, that returns 4 different `MotionValues`, we will use only one of those – `scrollYProgress` (vertical scroll progress between `0` and `1`).
2.  `useTransform` – hook, that creates a `MotionValue` that transforms the output of another MotionValue by mapping it from one range of values into another. In this demo, we will use the next set of props to pass to the hook:

-`scrollYProgress` to sync vertical page scroll with animation;

-`[0, 1]` – range of `scrollYProggress` when animation plays;

-`[“0%”, “-100%”]` – range of x to transform during vertical scroll change.

Long story short, hook transforms `x` (horizontal transform value) of the `motion` component while `scrollYProgress` is in range between 0 and 1 (from start of the page to the very bottom) from `0%` to –`100%`. 

It’s not in any way a full explanation of how the hook works and what props it can take. Check out the full documentation for useTransform.

3.To replicate in some way GreenSock’s ScrollTrigger – pinning **motion** component in viewport while animating we need to wrap it into two wrappers, outer wrapper has to have height > viewport `height` and inner wrapper has to have `position: sticky` and `top: 0`; `position: fixed`, depending on the animation, might work as well.

Check out the full styling down below ⬇️ or in the sandbox demo.

```js
import { motion, useTransform, useViewportScroll } from 'framer-motion';
 
export const Component = () => {
 const { scrollYProgress } = useViewportScroll();
 
 const x = useTransform(scrollYProgress, [0, 1], ['0%', '-100%']);
 
 return (
   <div style={{ height: '300vh' }}>
     <div
       style={{
         position: 'sticky',
         top: 0,
         height: '100vh',
         width: '100%',
         overflow: 'hidden',
       }}
     >
       <motion.p style={{ x }}>
         Rainbow Rainbow Rainbow
       </motion.p>
     </div>
   </div>
 );
};
```
{% embed https://codesandbox.io/embed/scrlanim-forked-7j572j?fontsize=14&hidenavigation=1&theme=dark %}

You probably already noticed the main issues with this kind of Scroll Triggered animations. First of all, animations play through the scroll of the whole page and not a portion of it. And the second problem, is that animating `x` from `0%` to `-100%` makes `motion` component to scroll out from the viewport (partially or fully, depending on the viewport width) before scroll of the page ends.

Let’s fix these issues in the next chapter ⬇️ by using a custom hook.

## Custom useElementViewportPosition hook
To address the issue, described in the previous chapter ⬆️, let’s create a custom hook that will allow us to calculate the range of viewports in which an element is visible.

Custom `useElementViewportPosition` returns `position` – value for second prop of `useTransform` hook – range between 0 and 1 (for example, `position = [0.2, 0.95]` means, that range is between 20% to 95% of the viewport).

```js
// * based on: https://gist.github.com/coleturner/34396fb826c12fbd88d6591173d178c2
function useElementViewportPosition(ref: React.RefObject<HTMLElement>) {
 const [position, setPosition] = useState<[number, number]>([0, 0]);
 
 useEffect(() => {
   if (!ref || !ref.current) return;
 
   const pageHeight = document.body.scrollHeight;
   const start = ref.current.offsetTop;
   const end = start + ref.current.offsetHeight;
 
   setPosition([start / pageHeight, end / pageHeight]);
   // eslint-disable-next-line react-hooks/exhaustive-deps
 }, []);
 
 return { position };
}
```
To figure out the third prop of the `useTransform` we need to calculate `carouselEndPosition` – end value to transform to. It basically `motion` component’s width subtracts the window’s width. Check out the detailed calculations in the sandbox demo.

```js
import { motion, useTransform, useViewportScroll } from 'framer-motion';
 
export const Component = () => {
 const ref = useRef<HTMLDivElement>(null);
 const carouselRef = useRef<HTMLDivElement>(null);
 const { position } = useElementViewportPosition(ref);
 const [carouselEndPosition, setCarouselEndPosition] = useState(0);
 const { scrollYProgress } = useViewportScroll();
 const x = useTransform(scrollYProgress, position, [0, carouselEndPosition]);
 
  useEffect(() => {
   // calculate carouselEndPosition
 }, []);
 
 return (
   <div>
     {/* content */}
     <section ref={ref}>
       <div className="container" style={{ height: "300vh" }}>
         <div className="sticky-wrapper">
           <motion.div ref={carouselRef} className="carousel" style={{ x }}>
             {[0, 1, 2, 3, 4].map((i) => (
               <div key={i} className="carousel__slide">
                 {i + 1}
               </div>
             ))}
           </motion.div>
         </div>
       </div>
     </section>
     {/* content */}
   </div>
 );
};
```
Now starting and ending points of animations, as well as transforming values, make the demo look more intentional and well-thought.
{% embed https://codesandbox.io/embed/scrlanm2-forked-73nq7q?fontsize=14&hidenavigation=1&theme=dark %}

I’m not entirely sure that this is the best way to handle this type of the animation with **framer-motion** but it’s the best I could came up with at this moment. I’m very curious how others solve this. For example, CodeSandbox projects have really fun Scroll Linked animations landing and it looks like it might be built with **framer-motion**.

## Reduce bundle size with LazyMotion

Probably every React ⚛️ developer at some point in their career has to face inflated bundle size problems. Shipping less JavaScript would most likely fix this issue.

To figure out the bundle size of your project and what can be improved, I recommend webpack-bundle-analyzer. After analyzing your bundle, you could find that you have giant package only purpose of which is to make few insignificant changes that could easily replaced with some kind of vanilla solution.

The same concern may be related to animation libraries, which usually have quite significant sizes. If your project is not heavily dependent on complicated animations and/or has bundle size or performance issues, you might consider to partially or fully transfer to CSS animations or even get rid of animations completely. Get your priorities straight: is it really worth using heavy jaw-dropping animations that make most of the low and mid-tear devices lag, overload or overheat? It might or it might not, depending on the project, its purpose and audience.

Fortunately, **framer-motion** got us covered!

`LazyMotion` is a very handy component that can help us to reduce bundle size. It synchronously or asynchronously loads some, or all, of the `motion` component’s features.

Documentation states, that default use of `motion` component adds around 25kb to the bundle size vs under 5kb for `LazyMotion` and `m` components.

{% embed https://codesandbox.io/embed/frmredbun-forked-cbc0z2?fontsize=14&hidenavigation=1&theme=dark %}

## Synchronous loading
How exactly can we achieve up to 5x bundle size reduction? With synchronous loading. By wrapping animation component with `LazyMotion` and passing needed features (`domAnimation` or `domMax`) to the features prop. The last step is to replace the regular `motion` component with its smaller twin – `m` component.

```js
import { LazyMotion, domAnimation, m } from 'framer-motion';
 
export const MotionBox = ({ isAnimating }: { isAnimating: booolean }) => (
 <LazyMotion features={domAnimation}>
   <m.div animate={isAnimating ? { x: 100 } : { x: 0 }} />
 </LazyMotion>
);
```

## Asynchronous loading
We can save users a few more KB by using async loading of `LazyMotion` `features`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eicag0wshsszy75c33pp.gif)

```js
// dom-max.ts
 
export { domMax } from 'framer-motion';
 
// modal.tsx
 
import { LazyMotion, m } from 'framer-motion';
 
const loadDomMaxFeatures = () =>
 import('./dom-max').then(res => res.domMax);
 
export const Modal = ({ isOpen }: { isOpen: booolean}) => (
 <AnimatePresence exitBeforeEnter initial={false}>
   {isOpen && (
     <LazyMotion features={loadDomMaxFeatures} strict>
       <m.div
         variants={ { open: { opacity: 1 }, collapsed: { opacity: 0 } } }
         initial="collapsed"
         animate="open"
         exit="collapse"
       >
         // modal content
       <m.div>
     </LazyMotion>
   }
 </AnimatePresence>
);
```
📎 `AnimatePresence` component is not required in this case, but it’s pretty much a necessity for any animations on component unmount. You can learn more about `AnimatePresence` here. 
Modals, accordions, carousels, and pretty much any other animation that requires a user’s interaction can benefit from that.

## Bundle size test
I tested how much LazyMotion can decrease bundle size on our live projects (loading features synchronously):

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1btao2gw1cm316b1zoah.png)

It’s not much, but I’m pretty happy with the results. Considering how little effort it takes to implement, a 5-10KB reduction in bundle size is quite significant.

## Layout animations
To automatically animate `layout` of motion component pass `layout` prop.

```js
<motion.div layout />
```
It doesn’t matter which CSS property will cause layout change (width, height, flex-direction, etc) `framer-motion` will animate it with transform to ensure best possible performance.

```js
import { useCallback, useState } from 'react';
import { motion } from 'framer-motion';
 
const flexDirectionValues = ['column', 'row'];
 
export const LayoutAnimationComponent = () => {
 const [flexDirection, setFlexDirection] = useState(flexDirectionValues[0]);
 
 const handleFlexDirection = useCallback(({ target }) => {
   setFlexDirection(target.value);
 },[]);
 
 return (
   <div>
     {flexDirectionValues.map((value) => (
       <button
         key={value}
         value={value}
         onClick={handleFlexDirection}
       >
         {value}
       </button>
     ))}
     <div style={{ display: 'flex', flexDirection }}>
       {[1, 2, 3].map((item) => (
         <motion.div layout key={item} transition={{ type: 'spring' }} />
       ))}
     </div>
   </div>
 );
};
```
{% embed https://codesandbox.io/embed/layan-forked-qzz2zg?fontsize=14&hidenavigation=1&theme=dark %}

At first, I didn’t realize how easy, useful and multifunctional `layout` animations are, but it’s truly rare!

## Shared layout animations
Similarly to layout animations, shared layout animations give us the opportunity to automatically animate `motion` components between each other. To do it, simply give multiple `motion` components same `layoutId` prop.

```js
import { motion } from 'framer-motion';
 
const bullets = [1, 2, 3];
 
export const Bullets = ({ currentIndex }: { currentIndex: number }) => {
 return (
   <div className="bullets">
     {bullets.map((bullet, index) => (
       <div key={index} className="bullet">
         <button>{bullet}</button>
         {index === currentIndex && <motion.div layoutId="indicator" className="indicator" />
          )}
       </div>
     ))}
   </div>
 );
};
```
Take a look at the turquoise dot under the slider’s navigation/bullet button and try to navigate. When a dot is animated by smoothly moving from one bullet to another, it looks like the same element, but it’s actually not!
{% embed https://codesandbox.io/embed/frmshare-forked-t87rfd?fontsize=14&hidenavigation=1&theme=dark %}

This particular demo might seem a bit excessive, considering our point of interest here is only one little dot. But it also uses `AnimatePresence` component to perform exit animation before the component unmounts, which might be extremely useful in so many different use cases. Check out Sandbox demo to see how to use AnimatePresence.

**Conclusion**
Since first time I used framer-motion (December 2019), it continued to grow and evolve and now this library gives us opportunity to easily bring to life almost any animation.

I feel like with animations it’s really easy to mess up at least some if not all aspects of good UI/UX experience. But **framer-motion** comes to the rescue with:

- Maintaining HTML and SVG semantics
- declarative and keyframes animations
- considerations to accessibility (`MotionConfig` + `reducedMotion`)
- bundle size reduction (`LazyMotion` + `m`)
- layout transitions (`layout` and `layoutId`)
- scroll triggered (`whileInView`) and scroll linked animations (`useViewportScroll`).

From a web developer’s perspective, working with `framer-motion` is very enjoyable and fun. It might or it might be the same for you, try it out to see for yourself, fortunately, it’s very easy to start with.

At the end of this article, I want to give you one last friendly reminder: animation is fun and exciting, but it is not a crucial element of a great user experience and – if not used intentionally and in moderation – a possible way to make it worse. You could get away with using simple CSS transitions or keyframes and still achieve excellent results without taking away much from your final product.