Skip to main content
Home / Articles /

Morphing SVG With react-spring

I’ve been intrigued by the morphing effect ever since I was a little kid. There’s something about a shape-shifting animation that always captures my attention. The first time I saw morphing left me wondering  “ Wow, how did they do that?” Since then, I’ve created demos and written an article about the effect.

There are a lot of options when it comes to different animation libraries that support morphing. Many of them are good and provide numerous features. Lately, I’ve been absorbed by react-spring. React-spring is a nifty physics-enabled animation library built on React. Adam Rackis recently posted a nice overview of it. The library comes with many features including, among a lot of others, SVG morphing. In fact, the beauty of react-spring lies in how it supports morphing. It allows you to do so directly in the markup where you define your SVG paths descriptors. This is good from a bookkeeping perspective. The SVG path descriptors are where you typically would expect them to be.

Here’s a video of what we’re looking into in this article:

It’s a morphing effect in an onboarding sequence. Here, it’s used as a background effect. It’s meant to complement the foreground animation; to make it stand out a bit more rather than taking over the scene.

Creating the SVG document

The first thing we want to do is to create the underlying model. Usually, once I have a clear picture of what I want to do, I create some kind of design. Most of my explorations start with a model and end with a demo. In most cases, it means creating an SVG document in my vector editor. I use Inkscape to draw my SVGs.

When I create SVG documents, I use the exact proportions. I find that it’s better to be precise. For me, it helps my perception of what I want to create when I use the same coordinate system in the vector editor as in the browser and the code editor. For example, let’s say you’re about to create a 24px ✕ 30px SVG icon, including padding. The best approach is to use the exact same size — an SVG document that is 24 pixels wide by 30 pixels tall. Should the proportions turn out to be wrong, then they can always be adjusted later on. SVG is forgiving in that sense. It’s scalable, no matter what you do.

The SVG document we’re creating is 256 pixels wide and 464 pixels high.

Drawing the models

When creating models, we need to think about where we place the nodes and how many nodes to use. This is important. This is where we lay the groundwork for the animation. Modeling is what morphing is all about. We have one set of nodes that transforms into another. These collections of nodes need to have the exact same number of nodes. Secondly, these sets should somehow correlate.

If the relation between the vector shapes is not carefully thought through, the animation won’t be perfect. Each and every node will affect the animation. Their position and the curvature need to be just right. For more detailed information on how nodes in SVG paths are constructed, refer to the explanation of Bézier curves on MDN.

Secondly, we need to take both shapes into account. One of the vectors may contain parts, which cannot be found in the other. Or, there may be other differences between the two models. For these cases, it can be a good idea to insert extra nodes in certain places. The best is to form strategies. Like, this corner goes there, this straight line bulges into a curve and so on.

I’ve put together a pen to illustrate what it looks like when sets correlate bad versus when accurately designed. In the below example, in the morphing effect on the left, nodes are randomly placed. The nodes that make up the number one and two have no relation. In the right example, the placement of the nodes is more carefully planned. This leads to a more coherent experience. 

The first model

The line tool is what we use to draw the first vector shape. As the model we’re creating is more abstract, it’s slightly more forgiving. We still need to think about the placement and the curvature, but it allows for more sloppiness.

As for vectors and sizing, the same goes for creating the models for morphing. It’s an iterative process. If it’s not right the first time, we can always go back and adjust. It usually requires an iteration or two to make the animation shine. Here’s what the model looks like when completed.

The result is a smooth abstract SVG shape with eight nodes.

The second and third models

Once the first model is complete, it’s time to draw the second model (which we can also think of as a state). This is the shape the first set will morph into. This could be the end state, i.e., a single morphing effect. Or it could be a step on the way, like a keyframe. In the case we’re looking at, there are three steps. Again, each model must correlate to the previous one. One way to make sure the models match up is to create the second vector as a duplicate of the first. This way, we know the models have the same numbers of nodes and the same look and feel.

Be careful with the editor. Vector editors typically optimize for file size and format. It might very well make the models incompatible when saving changes. I’ve made a habit of inspecting the SVG code after saving the file. It also helps if you’re familiar with the path descriptor format. It is a bit cryptic if you’re not used to it. It could also be a good idea to disable optimization in the vector editor’s preferences.

Repeat the above process for the third shape. Copy, relocate all of the nodes, and set the third color.

Lights, camera… action!

Once the models are created, we’ve done most of the work. Now it’s time to look at the animation part. React-spring comes with a set of hooks that we can use for animation and morphing. useSpring is a perfect candidate for the effect in this example. It’s meant to be used for single animations like the one we’re creating. Here’s how to initiate animations with the useSpring hook.

const [{ x }, set] = useSpring(() => ({
  x: 0,

The above gives us an animation property, x, to build our morphing effect around. A great thing about these animation properties is that we can alter them to create almost any kind of animation. If the value is off, we can change it through interpolation.

The second parameter, the set function, allows us to trigger updates. Below is a snippet of code showing its use. It updates the animation value x with a gesture handler useDrag from the react-use-gesture library. There are many ways in which we can trigger animations in react-spring.

const bind = useDrag(
  ({ movement: [x] }) => {
    // ...
    set({ x });

We now have everything set up to combine our models, the path descriptors, with the markup. By adding the animated keyword to the JSX code, we activate react-spring’s animation system. With the interpolation call to, previously named interpolate, we convert drag distances to morphed shapes. The output array contains the models already discussed. To get them in place, we simply copy the path descriptors from the SVG file into the markup. Three different descriptors, d, from three different path elements copied from three different SVG files are now combined into one. Here’s what the JSX node looks like when powered with a react-spring animation.

<svg ...>
      range: [-400, -200, 0],
      output: [
        // First model
        "M 157.81292,131.16918 C 128.33979,127.45582 59.004493,121.76045 53.287478,168.06051 47.570462,214.36057 86.454799,213.14326 77.881699,234.66986 69.308599,256.19646 59.042495,268.13837 67.634107,288.98209 76.225718,309.82581 103.27857,320.05328 138.34249,312.55156 173.40641,305.04984 204.93111,298.87002 208.02612,279.75926 211.12113,260.6485 189.48716,257.88808 188.5557,229.54606 187.62424,201.20404 212.01456,174.45091 200.8528,155.7634 189.69104,137.07589 187.28605,134.88254 157.81292,131.16918 Z",
        // Second model
        "M 157.81292,131.16918 C 128.33979,127.45582 48.756902,138.1566 53.287478,168.06051 57.818054,197.96442 75.182448,197.77187 73.782662,224.42227 72.382877,251.07266 70.314846,257.89078 72.757903,278.7345 75.20096,299.57822 88.114636,303.32873 113.94876,307.60312 139.78288,311.87751 159.84171,314.24141 176.25858,295.13065 192.67546,276.01989 203.83379,256.86332 190.60522,228.5213 177.37665,200.17928 205.866,189.8223 211.10039,171.13479 216.33478,152.44728 187.28605,134.88254 157.81292,131.16918 Z",
        // Third model
        "M 157.81292,131.16918 C 128.33979,127.45582 86.672992,124.83473 71.733144,166.01099 56.793295,207.18725 69.033893,203.92043 80.955976,230.57083 92.87806,257.22123 55.968217,259.9403 59.436033,279.75926 62.90385,299.57822 94.985717,299.83924 132.0922,306.16316 169.19868,312.48708 186.48544,320.38997 198.80328,288.98209 211.12113,257.57422 199.73475,245.59097 195.72902,217.24895 191.72328,188.90693 209.96504,178.54995 215.19943,159.86244 220.43382,141.17493 187.28605,134.88254 157.81292,131.16918 Z",

Let’s look at the differences between a standard JSX path element and what we currently have. To get the morphing animation in place, we have:

  • added the animated keyword to make the JSX path element animate with React spring,
  • changed the descriptor d from a string to a React spring interpolation, and
  • converted the distance x to a keyframe animation between three path descriptors.

Development environment

I have yet to find the perfect development environment for working with SVG. Currently, I go back and forth between the vector editor, the IDE, and the browser. There’s a bit of copying and some redundancy involved. It’s not perfect, but it works. I have experimented a bit, in the past, with scripts that parse SVGs. I have still not found something that I can apply to all scenarios. Maybe it’s just me who got it wrong. If not, it would be great if web development with SVGs could be a bit more seamless.

Here we go!

Last but not least, the demo!

Thanks for reading!