React SVG Animation (with React Spring) #2
Tom Dohnal
Posted on December 20, 2020
Are you a video learner? This blog post is also available in a video form on YouTube:
Look at the animation below 👇
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
- How to Prepare the SVG for Animation?
- Why the "Naive" Approach Does NOT Work?
- How Does transform-origin Work in SVG?
- Final touches
- 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>
)
}
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>
{/* ... */}
{/* ... */}
</>
)
}
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>
</>
)
}
The code looks quite okay in my opinion. Nonetheless, if you trigger the animation you'll be unpleasantly surprised as you'll see this:
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. 👇
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. 👇
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. 👇
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>
))
// ...
// ...
}
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 👇
What we'd like it to look like 👇
The latter animation has got more of a playful feel to it. It boils down to two things.
- Staggered effect (icons animating one after another)
- 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
})),
)
// ...
// ...
}
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)
},
})),
)
// ...
// ...
}
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
Posted on December 20, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.