React SVG Animation (with React Spring) #2

tomdohnal

Tom Dohnal

Posted on December 20, 2020

React SVG Animation (with React Spring) #2

Are you a video learner? This blog post is also available in a video form on YouTube:

Look at the animation below 👇

private data related icons rotating and scaling up and down next to a woman standing

You'll learn how to create this SVG animation in React Spring and what's tricky about animating transform: scale(...) and transform: rotate(...) in SVG.

(Full source code available on CodeSandbox)

Table of Contents

  1. How to Prepare the SVG for Animation?
  2. Why the "Naive" Approach Does NOT Work?
  3. How Does transform-origin Work in SVG?
  4. Final touches
  5. Bonus: How to do this in Framer motion and GSAP?



How to Prepare the SVG for Animation?

I assume you already know how to include SVGs in React. If not, check out my previous post (or YouTube video) where I explain everything from scratch.

For this animation, we're using an SVG I found on the unDraw website. After you download the SVG and convert the SVG to JSX, go ahead and locate the three icons in the SVG code and create separate React components out of them. (So that it's easier to work with.)

You should end up with something like this:

import React from 'react'

function Icon1() {
    return <g>{/* ... */}</g>
}
function Icon2() {
    return <g>{/* ... */}</g>
}
function Icon3() {
    return <g>{/* ... */}</g>
}

function App() {
    const icons = [<Icon1 />, <Icon2 />, <Icon3 />]

    return (
        <svg /* ... */>
            {icons}
            {/* ... */}
            {/* ... */}
        </svg>
    )
}
Enter fullscreen mode Exit fullscreen mode

Next, add a toggle button which will trigger the enter/leave animation

import React, { useState } from 'react'

// ...
// ...

function App() {
    const [toggle, setToggle] = useState(false)

    // ...
    // ...

    return (
        <>
            <button
                type="button"
                onClick={() => {
                    setToggle(!toggle)
                }}
            >
                Toggle animation
            </button>
            {/* ... */}
            {/* ... */}
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

You're now ready to start animating!

You can see the code for this section on CodeSandbox:



Why the "Naive" Approach Does NOT Work?

If you're familiar with animating HTML elements (or you've read the previous tutorial), you might think: "Oh, this animation is kinda easy, I'm just gonna use transform: rotate(...) scale(...) on the icon components and I'll be good to go".

Honestly, it seems like the perfectly reasonable thing to do. So pull up React Spring (or your favourite animation tool) and give it a try:

// ...
import { animated, useSprings } from 'react-spring'

// ...
// ...

function App() {
    // ...
    // ...

    const springs = useSprings(
        3,
        icons.map(() => ({
            transform: toggle ? 'rotate(0deg) scale(1)' : 'rotate(360deg) scale(0)',
        })),
    )

    const animatedIcons = springs.map((style, index) => (
        <animated.g style={style}>{icons[index]}</animated.g>
    ))

    return (
        <>
            {/* ... */}
            <svg /* ... */>
                {animatedIcons} {/* `animatedIcons` instead of `icons` */}
                {/* ... */}
            </svg>
        </>
    )
}
Enter fullscreen mode Exit fullscreen mode

The code looks quite okay in my opinion. Nonetheless, if you trigger the animation you'll be unpleasantly surprised as you'll see this:

icons rotating around the top-left corner rather than around their own centres

While the icons are being animated, they all seem to be rotating around a single point rather than each being rotated individually around their own centre points.

That's weird, you think. You might even have used transform: rotate(...) or transform: scale(...) for animating HTML elements and you're sure that it worked just fine. Yes, you're right, this would work just fine if we were animating HTML elements. But animating SVG elements is a bit trickier...

You can see the source code for this section on Codesandbox:



How Does transform-origin Work in SVG?

If you look at the (somewhat broken) animation above, you'll notice that the items are rotating around a point somewhere in the top-left corner of the screen. That's not what you want, you want the icons to be rotating around their own centre points. Is there a way to do that? The thing you're looking for is called transform-origin

According to MDN web docs, the transform-origin property "sets the origin for an element's transformations". Well, what does it really do? I think it's best demonstrated in a combination with transform: rotate(...) where it specifies the point which the element you're animating should revolve around.

How come you've never needed transform-origin for animating HTML elements? It's because its default value is 50% 50% (could be written as center) for HTML elements which corresponds to their middle points. For SVG elements, however, the default value is 0, 0 which corresponds to the top-right point in the SVG's viewbox.

Look at animation below to see how animating transform: rotate(...) with default transform-origin works in SVG. 👇
an animation of what happens when you apply the default transform-origin for SVG

The first idea on how to fix you might have could be this: If the default value of transform-origin is 0, 0 for SVGs and 50% 50% (could be written as center) for HTML elements, let's manually set the transform-origin to center for the SVG and it's gonna behave how I want (rotate around the centres of the elements).

If you were to try it, however, you wouldn't like the result. That's because setting transform-origin: center works differently for HTML and SVG elements. For HTML elements, it sets the origin point to the centre of the actual element you're animating. For SVG element it sets the origin point to the centre of the viewbox which contains your SVG element.

Have a look at the animation below to get a visual intuition for what's going on. 👇

an animation of what happens when you apply transform-origin: center for SVG

After all the struggle it seems like there is not an easy solution for making SVG elements rotate around their own centre points. If you think that, I've got good news for you. It can be fixed by adding one extra line of CSS. Add transform-box: fill-box in addition to transform-origin: center and watch the animation behave just like you wanted!

The transform-box property specifies what the transform-origin property should relate to. Its default value is transform-box: view-box which makes the transform-origin relate to the SVG viewbox. If you set it to transform-box: fill-box, it's going to relate to the element it's applied to. So the centre of the rotation is going to be the centre of the element you're rotating rather than the centre of the viewbox.

Have a look at the animation below for a more visual explanation. 👇

an animation of what happens when you apply transform-box: fill-box for SVG

Now that we've dived deep into the SVG transform intricacies, it's time to apply what we've learnt to our SVG animation:

// ...
// ...

function App() {
    // ...
    // ...

    const animatedIcons = springs.map((style, index) => (
        <animated.g
            style={{
                transformOrigin: 'center', // <- make it centre
                transformBox: 'fill-box', // <- of the element
                ...style,
            }}
        >
            {icons[index]}
        </animated.g>
    ))

    // ...
    // ...
}
Enter fullscreen mode Exit fullscreen mode

You can see the source code of this section on CodeSandbox

(One downside to using transform-box: fill-box is that it is not supported in legacy browsers (IE11). You could still achieve setting the transform-origin to the centre of the element by using exact pixel values like this: transform-origin: 120px 160px where 120px 160px would be the centre of the SVG element.)



Final touches

Compare the animation we've got at this point with what we'd like it to look like:

What we've got 👇

the animation that we've programmed so far

What we'd like it to look like 👇

the animation that we want to have

The latter animation has got more of a playful feel to it. It boils down to two things.

  1. Staggered effect (icons animating one after another)
  2. Wobbly effect (adding "bounciness")

Add staggered effect

To add the staggered effect, let's use the delay option passed to the useSpring function. (We used this approach in the previous tutorial, too.)

We leverage the fact that an index is passed to the .map(...) function and add a different delay for each of the icons:

// ...
// ...

function App() {
    // ...
    // ...

    const springs = useSprings(
        3,
        icons.map((_, index) => ({
            // ...
            // ...
            delay: index * 50, // 1st icon -> 0ms, 2nd icon -> 50ms, 3rd icon -> 100ms
        })),
    )

    // ...
    // ...
}
Enter fullscreen mode Exit fullscreen mode

Add wobbly effect

Adding the wobbly effect comes down to configuring the spring animation. More specifically, decreasing the friction. Friction controls the spring "resistance". The higher the friction, the less "bouncy" animation. The lower the friction, the more "bounciness".

It's usually best to fine-tune the exact numbers of the animation configuration manually.

// ...
// ...

function App() {
    // ...
    // ...

    const springs = useSprings(
        3,
        icons.map((_, index) => ({
            // ...
            // ...
            config: {
                friction: 16, // the default value is 26 (we *decrease* it to add bounciness)
            },
        })),
    )

    // ...
    // ...
}
Enter fullscreen mode Exit fullscreen mode

You can see the code for this section on CodeSandbox:



Bonus: How to do this in Framer motion and GSAP?

Everyone uses a different animation library. Nonetheless, the animation principles are pretty much the same. Some libraries do more of the manual work for you (e. g. Framer Motion which applies makes the "rotate" and "scale" animations work with SVG out-of-the-box), some are more bare-bones like React spring. Some libraries are framework specific (React Spring and Framer Motion) and some are more general (GSAP where you have to orchestrate your animation using useEffect and useRef).

If you're interested, have a look at the implementation of the same animation in React Spring, Framer Motion, and GSAP on Codesandbox

💖 💪 🙅 🚩
tomdohnal
Tom Dohnal

Posted on December 20, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related