State-of-the-art frontend development
In the third edition of the React SVG Animation series, we're going to create this ๐
(You can find a video version of this article on YouTube! ๐บ)
We're going to implement it by animating lines in SVG (the path
element) and we'll learn how to extract animation logic into re-usable custom hooks.
(Full source code available on CodeSandbox)
Before we start talking about the animation, we need to have something to animate.
After creating a new React app using your favourite tool (e. g. create-react-app
) and installing react-spring@next
using your favourite package manager, copy and paste this SVG. ๐
Note that we're using the
next
version of thereact-spring
library as the newest version (v9) is still in therc
stage.
function Image() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="286"
height="334"
fill="none"
viewBox="0 0 286 334"
>
<path
fill="#A78BFA"
stroke="#A78BFA"
strokeWidth="2"
d="M 143, 333 C 31.09 261.823 1 73.61 1 73.61 L 143 1 v 332 z"
/>
<path
fill="#8B5CF6"
stroke="#8B5CF6"
strokeWidth="2"
d="M 143, 333 C 254.911 261.823 285 73.61 285 73.61 L 143 1 v 332 z"
/>
<path
stroke="#4ADE80"
strokeWidth="24"
d="M75 153.5l68.081 77.5L235 97"
/>
</svg>
);
}
You can see that the SVG is comprised of three path
elements which correspond to the two left and right part of the "shield" and the checkmark.
Let's extract them into separate components so that it's easier for us to work with them independently.
First, grab the last path
element and create a Checkmark
Component:
function Checkmark() {
return (
<path stroke="#4ADE80" strokeWidth="24" d="M75 153.5l68.081 77.5L235 97" />
);
}
Next, we'd like to extract the left and right part of the shield. As the animation is identical for both parts of the shield, it's a good idea to create a ShieldPart
component which will accept a color
and a d
(path
definition) as props. We'll then pass the corresponding colour and path
definition to the ShieldPart
components.
function ShieldPart({ color, d }) {
return <path fill={color} stroke={color} strokeWidth="2" d={d} />;
}
Once you've created those components, put the inside the svg
instead of the path
elements.
<svg
// ...
>
{/* Left shield part */}
<ShieldPart
d="M 143, 333 C 31.09 261.823 1 73.61 1 73.61 L 143 1 v 332 z"
color="#A78BFA"
/>
{/* Right shield part */}
<ShieldPart
d="M 143, 333 C 254.911 261.823 285 73.61 285 73.61 L 143 1 v 332 z"
color="#8B5CF6"
/>
<Checkmark />
</svg>
We're now good to go and can start talking about the animation itself.
(You can see the source code for this section on CodeSandbox)
Let's have a proper look at the animations we're going to build. ๐๐
If you look really carefully, you can see that the animation consists of three parts.
First, the edges of the shield animate:
Then, the shield gets filled with colour:
Lastly, the checkmark animates:
Animating the shield "background" colour is quite straightforwardsโwe're justing going to animate the fill
property (an SVG equivalent of background
property) from #fff
(white) to the desired colour.
However, how do we go about animating the shield edges and checkmark? Well, we need a bit of "SVG trickery" to do that. Let's learn out it in the next section.
What do we even mean by "lines" in SVG? We do not mean the line
element but a path
element with a stroke
.
Let's use our "checkmark" path element as an example.
<path
stroke="#4ADE80" // defines the colour of the "line"
strokeWidth="24" // defines the width of the "line"
d="M75 153.5l68.081 77.5L235 97"
/>
Strokes in SVGs are similar to borders in HTML. The stroke
property defines the colour of the "line" (roughly equivalent to border-color
in HTML) and stroke-width
defines the "thickness" of the "line" (roughly equivalent to border-width
in HTML).
"What the heck does stroke
and stroke-width
have to do with animating the SVG," you might think. And you're right (partially ๐). We're going to animate neither of those properties but they do need to be present on the path
for the animation to make sense. If the path would only have the fill
property (something like background
in HTML) and not stroke
, we wouldn't be able to animate it.
Now that we've learnt about the prerequisites for the animation, let's move on and learn about another two properties (and these will actually be directly involved in the animation)โstroke-dasharray
and stroke-dashoffset
.
The stroke-dasharray
property is used to turn your "solid" line into a "dashed" line and defines how wide one "dash" is.
See the demonstration below. ๐
You can notice that if we set the range to its max value (which is equal to the length of the checkmark), the one "dash" covers the whole checkmark. This will be the key to the animation!
The stroke-dashoffset
property defines how much "shifted" the "dashes" are.
Have a look. ๐๐
You might have noticed that if you set the stroke-dasharray
property equal to the length of the path (which you can get using .getTotalLength()
), it appears as if there were no stroke-dasharray
set at all.
But is it really the case? Well, it certainly appears so, but it doesn't mean that it's the case. Actually, the line is still dashed, but the gap in the dashes is not visible as it's "after" the end of the checkmark.
What if we, though, combined stroke-dasharray
set to the length of the path with stroke-dashoffset
? What would it look like? ๐ค Let's have a look:
What?! How's that possible? It looks like what we've wanted to achieve! The checkmark is animating!
As the stroke-dashoffset
changes from 0 to the length of the checkmark, the checkmark is disappearing. That's because the "gap" (which's length is also equal to the length of the checkmark) gets "before" the "dash". If the stroke-dashoffset
is set to 0, only the "dash" part is visible. If it's set to the length of the checkmark, only the "gap" part is visible.
Therefore, to animate the checkmark, you have to:
stroke-dasharray
to its length (you can get it by .getTotalLength()
stroke-offset
from the length (obtained by .getTotalLength()
) to 0.Let's do that in the next section!
First, we need to find out the length of the path. You can either call the .getTotalLength()
function on the path
element and hard-code the value, or you can use useState
from React, and set the length of the path by passing a callback to the ref
property:
function Checkmark() {
const [length, setLength] = useState(null);
return (
<path
ref={(ref) => {
// The ref is `null` on component unmount
if (ref) {
setLength(ref.getTotalLength());
}
}}
// ...
/>
);
}
Next, we'll make the Checkmark
accept a toggle
property which will trigger the animation.
We'll also set its stroke-dasharray
equal to the length
that we keep track of.
Finally, we're going to animate the stroke-dashoffset
. We'll use the useSpring
hook for that. If the toggle is truthy, we'll set its value to 0
(the checkmark will appear). If it's falsy, we'll set it to the value of length
(the total length of the checkmark) and it'll disappear.
function Checkmark({ toggle }) {
const [length, setLength] = useState(null);
const animatedStyle = useSpring({
// we do *not* animating this property, we just set it up
strokeDasharray: length,
strokeDashoffset: toggle ? 0 : length,
});
return (
<animated.path
style={animatedStyle}
ref={(ref) => {
// The ref is `null` on component unmount
if (ref) {
setLength(ref.getTotalLength());
}
}}
// ...
/>
);
}
If you're not familiar with using
useSpring
,animated
, and React Spring in general, check out my previous posts about animating SVGs using React!
Finally, we need to pass the toggle
variable from our main Image
component down to the Checkmark
component.
We'll set it to false
initially and use the useEffect
hook together with setImmediate
to set it to true
once the component mounts and the checkmark length is measured (using the .getTotalLength()
).
function Image() {
const [toggle, setToggle] = useState(false);
useEffect(() => {
// `setImmediate` is roughly equal to `setTimeout(() => { ... }, 0)
// Using `setToggle` without `setImmediate` breaks the animation
// as we first need to allow for the measurement of the `path`
// lengths using `.getTotalLength()`
setImmediate(() => {
setToggle(true);
});
}, []);
return (
<svg
// ...
>
{/* ... */}
<Checkmark toggle={toggle} />
</svg>
);
}
(You can find the full source-code for this section on Codesandbox)
Thus far, we've only applied what we've learnt to the checkmark animation. However, a very similar animation could be applied to animate the edges of the shield.
That's why it might be a good idea to extract the logic of animating a "line" in SVG into a separate hook.
The hook is going to be responsible for measuring the path length and animating the path based on the toggle
variable.
So it's going to accept toggle
as an argument and return a style
variable (for the animation) and a ref
variable (for the path length measurement).
function useAnimatedPath({ toggle }) {
const [length, setLength] = useState(null);
const animatedStyle = useSpring({
strokeDashoffset: toggle ? 0 : length,
strokeDasharray: length,
});
return {
style: animatedStyle,
ref: (ref) => {
// The ref is `null` on component unmount
if (ref) {
setLength(ref.getTotalLength());
}
},
};
}
We're the going the use this hook in the Checkmark
component:
function Checkmark({ toggle }) {
const animationProps = useAnimatedPath({ toggle });
return (
<animated.path
{...animationProps}
// ...
/>
);
}
If you now refresh the page, the animation should look exactly the same as before this refactor.
Next, let's use the very same useAnimatedPath
hook for animating the edge of the shield in the ShieldPart
component.
// do *not* forget to make the `ShieldPart`
// component accept the `toggle` prop
function ShieldPart({ color, d, toggle }) {
const animationProps = useAnimatedPath({ toggle });
return (
<animated.path // `path` -> `animated.path`
{...animationProps}
// ...
/>
);
}
Finally, pass the toggle
prop onto the ShieldPart
components:
function Image() {
// ...
return (
<svg {/* ... */}>
{/* Left shield part */}
<ShieldPart
toggle={toggle}
// ...
/>
{/* Right shield part */}
<ShieldPart
toggle={toggle}
// ...
/>
{/* ... */}
</svg>
);
}
If you now refresh the page, you won't really be satisfied as you'll barely see the shield edges being animated.
That's because we're not animating the fill
(something like background
in HTML) of the shield and the colour of the shield edges match the colour of the shield background. Let's do it and finish the animation in the next section.
(You can find the full source code of the section on CodeSandbox)
First, let's tackle animating the fill
(something like background
in HTML) of the ShieldPart
component.
We'll use a useSpring
hook for the animation and will animate from #000
(white colour) when the toggle
is falsy to the color
property that the ShieldPart
component accepts when the toggle
property is truthy.
function ShieldPart({ color, d, toggle }) {
// rename: `animationProps` -> `animationStrokeProps`
const animationStrokeProps = // ...
const animationFillStyle = useSpring({
fill: toggle ? color : "#fff"
});
return (
<animated.path
{...animationStrokeProps}
// as the `animationStrokeProps` have a `style` property
// on it, it would be overriden by just passing
// `style={animationFillStyle}`
style={{
...animationStrokeProps.style,
...animationFillStyle
}}
// *remove* the `fill={color}`
// ...
/>
);
}
If you now refresh the page, the animation will look better. Just a little bit better, though. That's because everything is animating all at once. Instead, we want to animate the edges of the shield first, then fill the shield with colour and only then animate the checkmark.
In order to do that, let's leverage the delay
property which we can pass to the useSpring
function.
First, let's make our custom useAnimatedPath
accept a delay
as an argument:
function useAnimatedPath({ toggle, delay }) {
// ...
const animatedStyle = useSpring({
// ...
delay,
});
// ...
}
Next, let's set a delay
of 250
ms for the animation of fill
in the ShieldPart
component:
function ShieldPart({ color, d, toggle }) {
// ...
const animationFillStyle = useSpring({
// ...
delay: 250,
});
// ...
}
Finally, put a delay
of 500
to the useAnimatedPath
hook in the Checkmark
component:
function Checkmark({ toggle }) {
const animationProps = useAnimatedPath({
// ...
delay: 500,
});
// ...
}
Note that the exact numbers we put to the
delay
property are somewhat arbitrary. There is no right or wrong way number, just watch the animation and fine-tune the exact numbers to your liking
Hit refresh in your browser and the animation should look like this ๐๐
You can find the full source-code for this article on CodeSandbox!
Stay up to date with state-of-the-art frontend development.