Interactive Transform Animation with Framer Motion

Interactive Transform Animation with Framer Motion

02 May 2020

webreactstyled-componentsframer motionanimation

Overview

"framer-motion" is a great library offering smooth and easy to implement animations, transitions and much more. This library is an improvement on Framers previous library Pose which is now deprecated, which I used on a previous side project karma.audio.

Today we will be building a 3D-esq card that animates on hover.

Hover me!

I am so 3D

Framer Motion features

Below are some features I really like about framer-motion, you can see a full list on framers own marketing webpage Framer Motion

  • Smooth Animation: framer-motion handles transitions and animations outside of react and thus avoids react re-renders.
  • SSR compatible out of the box: I personally used framer in both a NextJS and now also in a Gatsby project and had zero problems with SSR.
  • Intuitive Syntax: I found the syntax to be really intuitive and really match nicely with react.
  • Interoperability with Styled Components: every framer motion element can easily be styled with CSS-in-JS libraries like Styled-Components
  • SVG Animations

Library Size

As all good things come with a cost so does Framer Motion. While I did see one or two hitches especially in more complicated cross transition animations my biggest gripe is the library size. Here you can see the build JS bundle sizes of this webpage and the very prominent purple framer motion block.

bundle sizes 1

Currently the inclusion of framer-motion adds about ~16kb when using compressed with gzip. I personally find this size to be reasonable for the sheer amount of features and the ease of use framer-motion offers.

Creating a Perspective Card

First of all make sure to install framer motion using yarn or npm in your project if you want to follow along this guide.

1npm install framer-motion

Framer provides so called motion components that augment default React HTML elements with extra features, if you already know styled-components this should be very familiar to you. So lets start by creating the framer version of a div.

1const Component = props => {
2 return <motion.div> Hello </motion.div>
3}

Styled Components and Framer Motion

But a div is just not enogh fun so lets improve on that with some styles. While motion components support className as well as style and therefore can be styled like any other react component I currently like mixing motion and styled components. So lets create a styled version of the very same motion.div

1const StyledMotionDiv = styled(motion.div)`
2 padding: 25px;
3 background-color: tomato;
4 color: white;
5 width: 250px;
6 text-align: center;
7`
8
9const Component = props => {
10 // here we replaced motion.div with StyledMotionDiv
11 return <StyledMotionDiv> Hello </StyledMotionDiv>
12}

Tracking mouse hover

Motion components expose all the normal HTML element event bindings as well as some custom ones for touch gestures. In this case we can utilizy the onPointerMove event callback to track mouse movement over our card component.

1const Component = props => {
2 // 1. create a new react state for storing last known mouse hover position
3 const [pos, setPos] = React.useState([0, 0])
4 // 2. create an event handler for the move event
5 const onMove = e => {
6 setPos([e.clientX, e.clientY])
7 }
8 return (
9 <>
10 {/* 3. Attach onMove to the onPointerMove event */}
11 <StyledMotionDiv onPointerMove={onMove}>Hello</StyledMotionDiv>
12 {/* 4. Print out current cursor position */}
13 Hover Position: (x: {pos[0]}, y: {pos[1]})
14 </>
15 )
16}

Animating

Now we just have to combine our tracking with framer motion animation. For that we first of all must convert the mouse coordinates to percentage values within the card. This requires two simple steps:

  1. We must adjust the origin of our mouse coordinates to match the left top corner of the card.
  2. The resulting coordinates must be divided by the width and height respectably, thus the bottom right corner when hovered should result in x = 1 y = 1.
  3. We should clamp the values to always be within the [0, 1] domain (Framer Motion can do this automatically so I will omit it in the sample below).
1const Component = props => {
2 const [pos, setPos] = React.useState([0, 0])
3
4 const onMove = e => {
5 // 1. get position information about the card
6 const bounds = e.currentTarget.getBoundingClientRect()
7 // 2. Transform the current pointer position so that it related
8 // to the top left corner of the Card.
9 // In addition make sure the value is between 0 and 1
10 const xValue = (e.clientX - bounds.x) / e.currentTarget.clientWidth
11
12 // 3. Perform same steps as for X Axis to the Y Axis
13 const yValue = (e.clientY - bounds.y) / e.currentTarget.clientHeight
14 setPos([xValue, yValue])
15 }
16 return (
17 <>
18 <StyledMotionDiv onPointerMove={onMove}>Hello</StyledMotionDiv>
19 Relative Hover Position: (x: {pos[0]}, y: {pos[1]})
20 </>
21 )
22}

With the transformed coordinates we are ready to performan the animation. We will use our x, y values to rotate the card along the respective axis resulting in a pretty convincing 3D push away effect. For this we will use Framer Motions useMotionValue and useTransform. useMotionValue is a hook that creates a MotionValue instance. The MotionValue object will track the state of our animation as well as additional data like velocity. useMotionValue reuses the same MotionValue instance between renders for a given component instance in a comparable way to the useState hook. useTransform transforms values of one domain into a different one. In our case it will transform our percentage coordinates into angles ranging from -angle to +angle. Since our code still produces coordinates outside our [0, 1] domain we must use clamp: true option for useTransform. We then use our rotateX and rotateY values within the style property of the card, motion will convert the MotionValues to CSS values. These values alone are not enough to create a 3D effect, for this to work properly we must add the perspective CSS property to a parent DOM node. Perspecitve will create a 3D-space for all its child elements, you can read more about it in this great article.

The below live example showcases the whole thing in action. Notice how the Relative Hover Position values are not updated on hover, this is because as we know Framer Motion does not use reacts render for the animations. You can use the sliders to adjust values or edit the code directly below.

Code & Playground

Hope you have some fun with framer motion and css perspecitve.

Wlad