DEV Community

Tom Dohnal
Tom Dohnal

Posted on • Updated on • Originally published at tomdohnal.com

React SVG Animation (with React Spring) #3

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)

Table of Contents

  1. How to prepare the SVG for the animation?
  2. How we're going to build the animation?
  3. How to animate lines in SVG?
  4. How to create a re-usable animation hook?
  5. Final touches



How to prepare the SVG for the animation?

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 the react-spring library as the newest version (v9) is still in the rc 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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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"
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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}
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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)



How we're going to build the animation?

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:
edges of the shield animating

Then, the shield gets filled with colour:
shield fill colour animating

Lastly, the checkmark animates:
shield checkmark animating

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.



How to animates lines in SVG?

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"
/>
Enter fullscreen mode Exit fullscreen mode

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. πŸ‘‡
changing dash width

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. πŸ‘€πŸ‘‡
changing dash offset

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:
checkmark animating as we're changing stroke dashoffset

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:
1) Set its stroke-dasharray to its length (you can get it by .getTotalLength()
2) Animate its stroke-offset from the length (obtained by .getTotalLength()) to 0.

Let's do that in the next section!

Animating path in React Spring

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());
        }
      }}
      // ...
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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());
        }
      }}
      // ...
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

(You can find the full source-code for this section on Codesandbox)



How to create a re-usable animation hook?

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());
      }
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

We're the going the use this hook in the Checkmark component:

function Checkmark({ toggle }) {
  const animationProps = useAnimatedPath({ toggle });

  return (
    <animated.path
      {...animationProps}
      // ...
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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}
      // ...
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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)



Final touches

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}`
      // ...
    />
  );
}
Enter fullscreen mode Exit fullscreen mode

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
  });

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

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
  });

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

Finally, put a delay of 500 to the useAnimatedPath hook in the Checkmark component:

function Checkmark({ toggle }) {
  const animationProps = useAnimatedPath({
    // ...
    delay: 500
  });

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

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!

Top comments (0)