Animating Figma's SVG Exports

When you're implementing a design that involves SVGs, you're pretty much always going to be using a generated piece of SVG code instead of hand-coding the SVG yourself. It's a lot easier to make SVGs in Figma than in code anyway, so why go the hard route?

Here's the thing: generated SVG code is often really hard to parse. If you can't make out what the SVG code is doing, how would you begin to animate it? Is there a way to export the SVG so that it's easy to animate?

Why yes! Kind of. In this post, I want to explore a few different techniques—both in Figma and in code—that will help in animating these exports.

Understanding Figma Exports

The main reason why Figma's exports are so hard to work with is that it typically jumbles the entire icon into a single <path> element.

This color swatch icon, for example, looks like this exported directly from Figma:

<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.4 16.5H7.6M11.3958 18.7499L16.6458 9.65667C17.4742 8.22179 16.9826 6.38702 15.5477 5.55859L13.3245 4.27504C12.8173 3.98221 12.1894 3.99558 11.6951 4.30975M11.0001 19.6742L18.8414 15.147C20.2763 14.3186 20.7679 12.4838 19.9395 11.0489L18.5187 8.58816C18.3086 8.22421 17.9203 8 17.5 8M12 16.5V6C12 4.34315 10.6569 3 9 3H6C4.34315 3 3 4.34315 3 6V16.5C3 18.9853 5.01472 21 7.5 21C9.98528 21 12 18.9853 12 16.5ZM8 16.5C8 16.7761 7.77614 17 7.5 17C7.22386 17 7 16.7761 7 16.5C7 16.2239 7.22386 16 7.5 16C7.77614 16 8 16.2239 8 16.5Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
*One minor tweak I did here was to change the stroke attribute to currentColor to make it work in dark mode.

While there is meaning behind this super-long string of numbers (check out my post on SVG paths to learn more!), it's nonetheless impossible to parse—how can you tell which part of the icon corresponds to which part of the path?

The way that Figma's exports work is by mapping each shape into a corresponding SVG element:

Frames are converted to <svg> elements with matching width and height:

<svg
width="128"
height="128"
viewBox="0 0 128 128"
>
...
</svg>

Rectangles, lines, and ellipses are exported as <rect>, <line>, and <ellipse> (or <circle>) elements, respectively:

  1. 0
  2. 12
  3. 24
  1. 0
  2. 12
  3. 24

Frame 1

<svg viewBox="0 0 24 24">
  <rect
    x=":x"
    y=":y"
    width=":width"
    height=":height"
    fill="white"
  />
</svg>
Try interacting with the shape to see how the code changes!

All other shapes, including arrows, polygons, and vectors, are exported as <path> elements:

<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18 6C18 5.44771 17.5523 5 17 5L8 5C7.44772 5 7 5.44771 7 6C7 6.55228 7.44772 7 8 7H16V15C16 15.5523 16.4477 16 17 16C17.5523 16 18 15.5523 18 15L18 6ZM6.70711 17.7071L17.7071 6.70711L16.2929 5.29289L5.29289 16.2929L6.70711 17.7071Z"
fill="currentColor"
/>
</svg>

These export rules mean that if the icon is jumbled together into a single vector in Figma, it will be jumbled together into a single <path> element in the export.

  • vector

<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M8 12H6C4.89543 12 4 12.8955 4 14C4 15.1046 4.89543 16 6 16H16C17.1046 16 18 16.8955 18 18C18 19.1046 17.1046 20 16 20H12M12 12L13.7573 12C14.553 12 15.3161 11.684 15.8787 11.1213L19.5 7.49995C20.3284 6.67152 20.3284 5.32838 19.5 4.49996C18.6716 3.67156 17.3284 3.67156 16.5 4.49998L12.8787 8.12132C12.3161 8.68393 12 9.44699 12 10.2426V12Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

But if the icon is split into multiple vectors, the export, too, will be split into multiple <path> elements:

  • vector

  • vector

<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19.4142 4.41414C18.6332 3.63311 17.3668 3.63311 16.5858 4.41416L12.5858 8.41417C12.2107 8.78924 12 9.29795 12 9.82838V12H14.1716C14.702 12 15.2107 11.7893 15.5858 11.4142L19.5858 7.41413C20.3668 6.63307 20.3668 5.36674 19.5858 4.5857L19.4142 4.41414Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M8 12H6C4.89543 12 4 12.8954 4 14C4 15.1046 4.89543 16 6 16H16C17.1046 16 18 16.8954 18 18C18 19.1046 17.1046 20 16 20H12"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

Let's take a look at how we can leverage this insight to animate some icons!

Animating Parts of an Icon

One of the simplest icon animations you can do is animating individual parts of an icon. For example, this arrow icon that does a little bounce when you hover over it:

How might we implement this?

Like many icons in Figma, this icon is composed of a single vector:

  • vector

<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14 4H20V10M14 10L19.25 4.75M10 14L4.75 19.25M4 14V20H10"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

Since we want to move these arrows in different directions, we need to split the icon so that each arrow has its own <path> element.

Splitting the Vector in Figma

One way to do this in Figma is to duplicate the vector and then delete the points that make up the arrow you don't want:

  • vector

  • vector

With two vectors, we get two path elements in the export, which means we can animate them individually!

  • vector

  • vector

<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 14L4.75 19.25M4 14V20H10"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M14 4H20V10M14 10L19.25 4.75"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

Now, we can animate each individual arrow by using something like CSS animations:

svg:hover #top-right {
  --x: 2px;
  --y: -2px;
  animation: bounce 0.75s;
}

svg:hover #bottom-left {
  --x: -2px;
  --y: 2px;
  animation: bounce 0.75s;
}

@keyframes bounce {
  0% { transform: translate(0px, 0px); }
  50% { transform: translate(var(--x), var(--y)); }
  100% { transform: translate(0px, 0px); }
}

Constructing the SVG

I want to highlight that there are a number of different ways to construct the SVG from the two vector layers.

One way is the approach we took here—export the entire frame as an SVG:

  • expand-45

    • vector

    • vector

This is the approach I generally recommend because your output will look exactly like the original icon in Figma, but it does require some know-how of how to animate SVG elements.

Another approach I've seen is to export the vector layers individually, then reconstruct the SVG in code using position: absolute:

.wrapper {
  position: relative;
  width: 250px;
  aspect-ratio: 1;
}

#bottom-left {
  position: absolute;
  top: 50%;
  left: 0;
}

#top-right {
  position: absolute;
  top: 0;
  left: 50%;
}

.wrapper:hover #top-right {
  --x: 30px;
  --y: -30px;
  animation: bounce 0.75s;
}

.wrapper:hover #bottom-left {
  --x: -30px;
  --y: 30px;
  animation: bounce 0.75s;
}

@keyframes bounce {
  0% { transform: translate(0px, 0px); }
  50% { transform: translate(var(--x), var(--y)); }
  100% { transform: translate(0px, 0px); }
}

This approach works great if you're unfamiliar with SVGs because you can stay in the HTML world and use CSS's animation rules, but it requires some fiddling with position and size to get the icon to look right.

Neither approach is objectively better than the other; while I personally recommend the first approach, I recognize that some people are more efficient with the second!

Incomplete Vectors

Sometimes, splitting one vector into multiple vectors isn't enough to get your code into an "animateable" state. For example, consider this color swatch animation:

Hover me!

To make this animation work, we want to be able to individually change the rotation of the two color swatches in the back:

To support this, our Figma export needs to have three separate vectors: one for each color swatch.

Here's the catch: with the icon as it is, the two color swatches in the back aren't even complete color swatches!

Take the middle swatch, for example; if we delete the first and last swatches, we're left with this:

<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11.3965 18.7499L17.1465 8.79063C17.6988 7.83405 17.371 6.61087 16.4144 6.05858L12.5 3.79858"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

This is an issue because it means that splitting the vector alone wouldn't work—how can we animate something that's not even drawn in Figma?

Certainly, one way is to bug your designer, but another way is to recreate the icon in code. Here's the idea: because the icon is made up of three identical swatches, we'll export just the top swatch from Figma and create the rest in code.

Exporting the top swatch alone will give us a single path element that we can work with:

<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M7.4 16.5H7.6M12 16.5V5C12 3.89543 11.1046 3 10 3H5C3.89543 3 3 3.89543 3 5V16.5C3 18.9853 5.01472 21 7.5 21C9.98528 21 12 18.9853 12 16.5ZM8 16.5C8 16.7761 7.77614 17 7.5 17C7.22386 17 7 16.7761 7 16.5C7 16.2239 7.22386 16 7.5 16C7.77614 16 8 16.2239 8 16.5Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>

To make the middle swatch, we'll duplicate the <path> element and then rotate it around the circle:

<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
id="middle-swatch"
d="M7.4 16.5H7.6M12 16.5V5C12 3.89543 11.1046 3 10 3H5C3.89543 3 3 3.89543 3 5V16.5C3 18.9853 5.01472 21 7.5 21C9.98528 21 12 18.9853 12 16.5ZM8 16.5C8 16.7761 7.77614 17 7.5 17C7.22386 17 7 16.7761 7 16.5C7 16.2239 7.22386 16 7.5 16C7.77614 16 8 16.2239 8 16.5Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M7.4 16.5H7.6M12 16.5V5C12 3.89543 11.1046 3 10 3H5C3.89543 3 3 3.89543 3 5V16.5C3 18.9853 5.01472 21 7.5 21C9.98528 21 12 18.9853 12 16.5ZM8 16.5C8 16.7761 7.77614 17 7.5 17C7.22386 17 7 16.7761 7 16.5C7 16.2239 7.22386 16 7.5 16C7.77614 16 8 16.2239 8 16.5Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
#middle-swatch {
transform: rotate(30deg);
transform-origin: 7.5px 16.5px;
}

Here, we gave the middle swatch an id of middle-swatch and then used CSS to rotate it around the point (7.5, 16.5).

Before we move on to the next swatch, we need to fix something: the top swatch should cover the middle swatch. We can fix this by adding the fill attribute to the top swatch:

<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
id="middle-swatch"
d="M7.4 16.5H7.6M12 16.5V5C12 3.89543 11.1046 3 10 3H5C3.89543 3 3 3.89543 3 5V16.5C3 18.9853 5.01472 21 7.5 21C9.98528 21 12 18.9853 12 16.5ZM8 16.5C8 16.7761 7.77614 17 7.5 17C7.22386 17 7 16.7761 7 16.5C7 16.2239 7.22386 16 7.5 16C7.77614 16 8 16.2239 8 16.5Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M7.4 16.5H7.6M12 16.5V5C12 3.89543 11.1046 3 10 3H5C3.89543 3 3 3.89543 3 5V16.5C3 18.9853 5.01472 21 7.5 21C9.98528 21 12 18.9853 12 16.5ZM8 16.5C8 16.7761 7.77614 17 7.5 17C7.22386 17 7 16.7761 7 16.5C7 16.2239 7.22386 16 7.5 16C7.77614 16 8 16.2239 8 16.5Z"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
fill="var(--gray-1)"
/>
</svg>
#middle-swatch {
transform: rotate(30deg);
transform-origin: 7.5px 16.5px;
}

Keep in mind that adding the fill attribute here means the icon will no longer be transparent. It is, however, totally possible to keep the transparency while still having the top swatch cover the middle swatch—by using masks:

<svg
  width="350"
  viewBox="-0.5 -0.5 25 25"
  fill="none"
  xmlns="http://www.w3.org/2000/svg"
>
  <!-- grid background -->
  <defs>
    <pattern
      id="main-grid"
      patternUnits="userSpaceOnUse"
      width="4"
      height="4"
    >
      <g stroke-width="2" stroke="var(--gray6)">
        <line x1="4" y1="0" x2="4" y2="4" vector-effect="non-scaling-stroke"></line>
        <line x1="0" y1="4" x2="4" y2="4" vector-effect="non-scaling-stroke"></line>
      </g>
    </pattern>
  </defs>
  <rect stroke="var(--gray6)" vector-effect="non-scaling-stroke" width="24" height="24" fill="url(#main-grid)"></rect>
  <!-- mask -->
  <defs>
    <mask id="top-mask">
      <rect width="100%" height="100%" fill="white" />
      <path
        d="M7.4 16.5H7.6M12 16.5V5C12 3.89543 11.1046 3 10 3H5C3.89543 3 3 3.89543 3 5V16.5C3 18.9853 5.01472 21 7.5 21C9.98528 21 12 18.9853 12 16.5ZM8 16.5C8 16.7761 7.77614 17 7.5 17C7.22386 17 7 16.7761 7 16.5C7 16.2239 7.22386 16 7.5 16C7.77614 16 8 16.2239 8 16.5Z"
        fill="black"
      />
    </mask>
  </defs>
  <!-- icon -->
  <g mask="url(#top-mask)">
    <path
      id="middle-swatch"
      d="M7.4 16.5H7.6M12 16.5V5C12 3.89543 11.1046 3 10 3H5C3.89543 3 3 3.89543 3 5V16.5C3 18.9853 5.01472 21 7.5 21C9.98528 21 12 18.9853 12 16.5ZM8 16.5C8 16.7761 7.77614 17 7.5 17C7.22386 17 7 16.7761 7 16.5C7 16.2239 7.22386 16 7.5 16C7.77614 16 8 16.2239 8 16.5Z"
      stroke="currentColor"
      stroke-width="2"
      stroke-linecap="round"
      stroke-linejoin="round"
    />
  </g>
  <path
    d="M7.4 16.5H7.6M12 16.5V5C12 3.89543 11.1046 3 10 3H5C3.89543 3 3 3.89543 3 5V16.5C3 18.9853 5.01472 21 7.5 21C9.98528 21 12 18.9853 12 16.5ZM8 16.5C8 16.7761 7.77614 17 7.5 17C7.22386 17 7 16.7761 7 16.5C7 16.2239 7.22386 16 7.5 16C7.77614 16 8 16.2239 8 16.5Z"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
  />
</svg>

Masks are out of scope for this post, but if you're interested, you can check out the MDN documentation or my SVG course to learn more!

Great! There's just one final swatch to cover. Try adding it in the sandbox below:

<svg
  width="350"
  viewBox="0 0 24 24"
  fill="none"
  xmlns="http://www.w3.org/2000/svg"
>
  <g opacity="0.1">
    <use href="icon.svg#answer" />
  </g>
  <!-- Your code here -->
  <path
    id="middle-swatch"
    d="M7.4 16.5H7.6M12 16.5V5C12 3.89543 11.1046 3 10 3H5C3.89543 3 3 3.89543 3 5V16.5C3 18.9853 5.01472 21 7.5 21C9.98528 21 12 18.9853 12 16.5ZM8 16.5C8 16.7761 7.77614 17 7.5 17C7.22386 17 7 16.7761 7 16.5C7 16.2239 7.22386 16 7.5 16C7.77614 16 8 16.2239 8 16.5Z"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
  />
  <path
    d="M7.4 16.5H7.6M12 16.5V5C12 3.89543 11.1046 3 10 3H5C3.89543 3 3 3.89543 3 5V16.5C3 18.9853 5.01472 21 7.5 21C9.98528 21 12 18.9853 12 16.5ZM8 16.5C8 16.7761 7.77614 17 7.5 17C7.22386 17 7 16.7761 7 16.5C7 16.2239 7.22386 16 7.5 16C7.77614 16 8 16.2239 8 16.5Z"
    stroke="currentColor"
    stroke-width="2"
    stroke-linecap="round"
    stroke-linejoin="round"
    fill="var(--gray1)"
  />
</svg>

With this setup, we can make the animation by animating the rotation angle of each swatch!

#middle-swatch {
  transform: rotate(30deg);
  transform-origin: 7.5px 16.5px;
}
  
#bottom-swatch {
  transform: rotate(58deg);
  transform-origin: 7.5px 16.5px;
}

svg:hover #middle-swatch {
  animation: rotate 1s;
  --stop1: 45deg;
  --stop2: 16deg;
  --stop3: 30deg;
}

svg:hover #bottom-swatch {
  animation: rotate 1s;
  --stop1: 90deg;
  --stop2: 32deg;
  --stop3: 58deg;
}

@keyframes rotate {
  25% {
    transform: rotate(var(--stop1));
  }
  75% {
    transform: rotate(var(--stop2));
  }
  100% {
    transform: rotate(var(--stop3));
  }
}

A Deeper Dive into SVGs

Awesome!

In this post, we took a closer look at how Figma exports SVGs and how we can leverage this knowledge to make the exports easier to work with. In short:

  • vector

  • <path d="..." />

Having individual <path> elements that clearly map to different parts of the icon makes it much easier to animate the icon in code.

But what if you can't cleanly split the icon into multiple vectors? As we saw in the second example, you'll sometimes need to recreate parts of the icon in code to make it animateable. This is where things can get challenging because you have to be a bit familiar with SVGs to make things work.

If you're interested in getting your feet wet with SVGs, I've spent the last year or so creating a course that covers everything you need to know about SVGs and SVG animations. You can check it out right here:

Interactive SVG Animations ->