Go to homepage

Projects /

Animated SVG Icon

How to optimize SVG code and animate an SVG icon using CSS and Snap.svg library.

Animated SVG Icon
Check our new component library →

Working with SVG files is not an option anymore. With a huge amount of high definition devices out there, it's not sustainable to export different sizes of the same bitmap assets and target specific device resolutions through CSS media queries. We need to rely on vector graphics whenever it's possible.

Today's resource is a very simple icon, that we imported as inline SVG into our index.html file. After having optimized the svg code, we tried to push it one step further by animating the icon using CSS and Snap.svg, which is a javascript SVG library created by the Adobe team. To be honest, it's not that easy to animate svg files with the current browsers support status quo, but we learned some tricks along the way that we're happy to share with you!

If you're new to SVG, here are some great resources to start with:

Creating the SVG

The easiest way to create an SVG illustration is to use graphic editors such as Adobe Illustrator or Sketch. Pick the one you prefer and create an icon (or download our source file). Most of what you create in these apps is SVG friendly, meaning that the graphics you create can be translated into code. The easiest way to realize that is to save an icon as .svg file, then open it with a code editor.

Take in mind, though, that the way you organize your layers in your graphic editor is gonna affect the code output.

To make you an example: since our final icon is animated in 2 steps, I created 2 separate layers in Illustrator, one for the loading effect and one for the buildings. Since the visibility of the cd-buildings layer is turned off, I can expect a display:none applied to that element in the output code. Also, layers and groups in Illustrator are considered groups in the SVG code exported (which is a good thing). Layer titles in AI will be assigned as #id values in the SVG code.

SVG file in Illustrator

While exporting .svg from AI, default settings work just fine. I just make sure that CSS Properties is set to Style Elements, in order to separate style from content - AI automatically creates classes.

SVG save settings in AI

Some points to take in mind while creating SVGs in a graphic editor:

  • The way you organize/group layers will affect the output code.
  • Give layers a name.
  • Try to use simple shapes and remove unnecessary anchor points (in AI try object>Path>Simplify).
  • Try not to vectorize strokes, you can manage them later in your code, plus the code is cleaner.
  • Not all you do in a graphic editor is SVG compatible. AI has specific SVG filters, but browsers support is not great for most of them.

Optimizing SVG code

I personally don't use tools to automatically clean up SVG code for me. Since I always start from the graphic editor, I found out the best way to export clean code is to be thorough while designing - and following the tips I suggested before.

Once the .svg file has been exported, let's import it into a code editor and take a look at the code:

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="200px" height="200px" viewBox="0 0 200 200" style="enable-background:new 0 0 200 200;" xml:space="preserve">
   <style type="text/css">
      .st0{fill:none;stroke:#D7DCE0;stroke-width:4;stroke-miterlimit:10;}
      .st1{fill:none;stroke:#A84D54;stroke-width:4;stroke-miterlimit:10;}
      .st2{opacity:0;fill:#FFFFFF;}
      .st3{fill:#223443;}
      .st4{fill:#FFFFFF;stroke:#223443;stroke-width:4;stroke-miterlimit:10;}
      .st5{fill:none;stroke:#223443;stroke-width:4;stroke-miterlimit:10;}
   </style>
   <g id="cd-loading">
      <g id="cd-circle">
         <circle id="cd-loading-circle" class="st0" cx="100" cy="100" r="77.5"/>
         <circle id="cd-loading-circle-filled" class="st1" cx="100" cy="100" r="77.5"/>
      </g>
      <g id="cd-play-btn">
         <rect x="84" y="78.1" class="st2" width="32" height="43.9"/>
         <polygon class="st3" points="84,78.1 116,100 84,121.9     "/>
      </g>
      <g id="cd-pause-btn">
         <rect x="81" y="80.1" class="st2" width="38" height="39.9"/>
         <rect x="81" y="80.1" class="st3" width="11" height="39.9"/>
         <rect x="108" y="80.1" class="st3" width="11" height="39.9"/>
      </g>
   </g>
   <g id="cd-buildings">
      <g id="cd-home-3">
         <rect id="cd-home-3-base" x="66" y="55" class="st4" width="58" height="123"/>
         <polygon id="cd-home-3-roof" class="st4" points="59.4,56 95,15 130.6,56     "/>
         <circle id="cd-home-3-window" class="st4" cx="95" cy="77" r="11"/>
       </g>
<g id="cd-home-2"> <rect id="cd-home-2-base" x="98" y="132" class="st4" width="69" height="46"/> <rect id="cd-home-2-roof" x="98" y="122" class="st4" width="69" height="10"/> <rect id="cd-home-2-door" x="141" y="155" class="st4" width="15" height="22"/> <rect id="cd-home-2-window" x="106" y="143" class="st4" width="25" height="9"/> </g> <g id="cd-home-1"> <rect id="cd-home-1-base" x="29" y="109" class="st4" width="69" height="69"/> <rect id="cd-home-1-roof" x="23" y="100" class="st4" width="80" height="9"/> <rect id="cd-home-1-door" x="53" y="151" class="st4" width="20" height="27"/> <rect id="cd-home-1-window" x="53" y="121" class="st4" width="20" height="18"/> <rect id="cd-home-1-chimney" x="34" y="89" class="st4" width="11" height="11"/> </g> <line id="cd-floor" class="st5" x1="2" y1="178" x2="198" y2="178"/> <g id="cd-cloud-1"> <line class="st0" x1="5" y1="13" x2="29" y2="13"/> <line class="st0" x1="35" y1="13" x2="44" y2="13"/> <line class="st0" x1="72" y1="27" x2="14" y2="27"/> </g> <g id="cd-cloud-2"> <line class="st0" x1="191" y1="68" x2="159" y2="68"/> <line class="st0" x1="153" y1="68" x2="144" y2="68"/> <line class="st0" x1="179" y1="83" x2="133" y2="83"/> </g> </g> </svg>

As you can see, Illustrator created some classes for us. Also, the structure is pretty clean already. By giving a name to groups and layers in AI, we can now easily find them in the SVG coding structure.

First of all, since we want to include this graphic as inline SVG, we can grab only the code wrapped into the <svg></svg> tags and paste it into an .html file.

I'm working with a HTML5 document, so I can remove the namespace declaration and add a title and description to improve the file accessibility.

<body>
   <svg version="1.1" x="0px" y="0px" width="200px" height="200px" viewBox="0 0 200 200" style="enable-background:new 0 0 200 200;" xml:space="preserve">
      <title>Buildings</title>
      <desc>3 minimal buildings with floating clouds</desc>
      <style type="text/css">
         <!-- ... -->
      </style>
      <!-- SVG elements here -->
   </svg>
</body>

Next step is to create more semantic classes in CSS to replace the ones created by Illustrator. The icon is quite minimal, therefore the CSS is gonna be just few classes:

/* -------------------------------- 

Manage colors

-------------------------------- */
.cd-stroke {
  fill: none;
  stroke-width: 4;
  stroke-miterlimit: 10;
}

.cd-stroke-color-1 {
  stroke: #223443;
  fill: #f8f8f8;
}
.cd-stroke-color-1#floor {
  fill: none;
}

.cd-stroke-color-2 {
  stroke: #D7DCE0;
}

.cd-stroke-color-3 {
  stroke: #A84D54;
}

.cd-fill-color-1 {
  fill: #223443;
}

.cd-pointer {
  fill: #FFFFFF;
  opacity: 0;
}

Yes, we can modify SVG presentation attributes inside our CSS file! We can now delete the style from the SVG code and assign the classes accordingly to the elements.

Also, I did some changes to the structure: I moved some elements down in the SVG structure, the reason being that the lower an element is in the structure, the higher its z-index. Finally I changed some coordinates as well - very small details, like moving elements couple of pixels around if they weren't perfectly aligned. You know, unnecessary stuff designers love to waste time with.

<svg id="cd-animated-svg" version="1.1" x="0px" y="0px" width="200px" height="200px" viewBox="0 0 200 200" style="enable-background:new 0 0 200 200;" xml:space="preserve">
   <title>Buildings</title>
   <desc>3 minimal buildings with floating clouds</desc>
   <g id="cd-loading">
      <g id="cd-circle">
         <circle id="cd-loading-circle" class="cd-stroke cd-stroke-color-2" cx="100" cy="100" r="77.5"/>
         <circle id="cd-loading-circle-filled" class="cd-stroke cd-stroke-color-3" cx="100" cy="100" r="77.5"/>
      </g>
      <g id="cd-play-btn">
         <rect class="cd-pointer" x="84" y="78" width="33" height="44"/>
         <polygon class="cd-fill-color-1" points="84,78 116,100 84,122   "/>
      </g>
      <g id="cd-pause-btn">
         <rect class="cd-pointer" x="81" y="80" width="38" height="40"/>
         <rect class="cd-fill-color-1" x="81" y="80" width="11" height="40"/>
         <rect class="cd-fill-color-1" x="108" y="80" width="11" height="40"/>
      </g>
   </g>
   <g id="cd-buildings">
      <g id="cd-cloud-1">
         <line class="cd-stroke cd-stroke-color-2" x1="5" y1="13" x2="29" y2="13"/>
         <line class="cd-stroke cd-stroke-color-2" x1="35" y1="13" x2="44" y2="13"/>
         <line class="cd-stroke cd-stroke-color-2" x1="72" y1="27" x2="14" y2="27"/>
      </g>
      <g id="cd-cloud-2">
         <line class="cd-stroke cd-stroke-color-2" x1="191" y1="68" x2="159" y2="68"/>
         <line class="cd-stroke cd-stroke-color-2" x1="153" y1="68" x2="144" y2="68"/>
         <line class="cd-stroke cd-stroke-color-2" x1="179" y1="83" x2="133" y2="83"/>
      </g>
      <g id="cd-home-3">
         <polygon id="cd-home-3-roof" class="cd-stroke cd-stroke-color-1" points="59.4,55 95,15 130.6,55"/>
         <rect id="cd-home-3-base" class="cd-stroke cd-stroke-color-1" x="66" y="54" width="58" height="124"/>
         <circle id="cd-home-3-window" class="cd-stroke cd-stroke-color-1" cx="95" cy="77" r="11"/>
      </g>
      <g id="cd-home-2">
         <rect id="cd-home-2-base" class="cd-stroke cd-stroke-color-1" x="98" y="132" width="70" height="46"/>
         <rect id="cd-home-2-roof" class="cd-stroke cd-stroke-color-1" x="98" y="122" width="70" height="10"/>
         <rect id="cd-home-2-door" class="cd-stroke cd-stroke-color-1" x="141" y="156" width="16" height="22"/>
         <rect id="cd-home-2-window" class="cd-stroke cd-stroke-color-1" x="106" y="143" width="26" height="10"/>
      </g>
      <g id="cd-home-1">
         <rect id="cd-home-1-chimney" class="cd-stroke cd-stroke-color-1" x="34" y="89" width="11" height="11"/>
         <rect id="cd-home-1-base" class="cd-stroke cd-stroke-color-1" x="29" y="108" width="70" height="70"/>
         <rect id="cd-home-1-roof" class="cd-stroke cd-stroke-color-1" x="23" y="100" width="81" height="9"/>
         <rect id="cd-home-1-door" class="cd-stroke cd-stroke-color-1" x="53" y="150" width="20" height="28"/>
         <rect id="cd-home-1-window" class="cd-stroke cd-stroke-color-1" x="53" y="121" width="20" height="18"/>
      </g>
      <line id="cd-floor" class="cd-stroke cd-stroke-color-1" x1="2" y1="178" x2="198" y2="178"/>
   </g>
</svg>

The SVG is almost ready to be animated. I wrapped the <svg> element into a container (.cd-svg-container) to align the icon into the center. Setting a max-width:100% will make the SVG responsive (just like images).

Also you may notice an overflow:hidden applied to the SVG: it's because we will animate the clouds in and out the canvas, and for some unknown reasons IE shows them even when they are out, unless we apply the overflow property. Also you may notice a rectangle (.cd-pointer) inside the #cd-pause-btn element. It's used to make the area between the 2 pause rectangles clickable, hence the opacity:0 and the fill value.

We defined a .cd-fade-out class to be applied in javascript at the end of the loading animation to the #cd-loading element.

Finally we used the .no-js class in case javascript is disabled: as fallback we don't show the loading icon, but the buildings (it just makes more sense).

.cd-svg-container {
  width: 90%;
  max-width: 200px;
  margin: 0 auto 100px;
}
.cd-svg-container svg {
  display: block;
  overflow: hidden;
  max-width: 100%;
}
.no-js .cd-svg-container {
  height: 200px;
  background: url("../img/cd-icon.svg") no-repeat center center;
}
.no-js .cd-svg-container svg {
  display: none;
}

/* -------------------------------- 

Main elements - Loading

-------------------------------- */
#cd-loading {
  opacity: 1;
  visibility: visible;
  -webkit-transition: opacity .3s 0s, visibility 0s 0s;
  -moz-transition: opacity .3s 0s, visibility 0s 0s;
  transition: opacity .3s 0s, visibility 0s 0s;
}
#cd-loading.fade-out {
  opacity: 0;
  visibility: hidden;
  -webkit-transition: opacity .3s 0s, visibility 0s .3s;
  -moz-transition: opacity .3s 0s, visibility 0s .3s;
  transition: opacity .3s 0s, visibility 0s .3s;
}

#cd-play-btn, #cd-pause-btn {
  cursor: pointer;
}

#cd-pause-btn {
  pointer-events: none;
}

.play-is-clicked #cd-pause-btn {
  pointer-events: auto;
}

/* -------------------------------- 

Main elements - Buildings 

-------------------------------- */
#cd-home-1-chimney, #cd-home-3-roof {
  visibility: hidden;
}

Animating the SVG

We decided to animate our svg using JavaScript rather than SMIL in order to support Internet Explorer (if you are interested in the SMIL technique check out this guide by Sara Soueidan).
In our animation, we wanted to initially hide the #cd-buildings group. To do so, we modified some attributes in the svg code.
As an example, let's focus on the #cd-home-1-door rectangle element (which is the door element of the first house on the left). We wanted this element to be hidden at the beginning and then animate its height from 0 to 28px. So, in the HTML structure, we set the height = "0" (instead of height="28") and add a data-height = "28"  to easily retrieve it with JavaScript.

<rect id="cd-home-1-door" class="cd-stroke cd-stroke-color-1" x="53" y="150" width="20" height="0" data-height="28"/>

We used the animate() method provided by Snap.svg

var animatingSvg = Snap('#cd-animated-svg'),
    buildingDoor1 = animatingSvg.select('#cd-home-1-door');

buildingDoor1.animate({'height': buildingDoor1.attr('data-height')}, 300, mina.easeinout);

The door expands from top to bottom, but we wanted the opposite effect! So we applied a 180deg rotation to the element.

<rect id="cd-home-1-door" class="cd-stroke cd-stroke-color-1" x="53" y="150" width="20" height="0" transform="rotate(180 63 164)" data-height="28"/>

One important note: at the beginning we tried appling a CSS rotation (rather than using the svg transform attribute), but unfortunately CSS transformations on SVG are currently not supported in IE11 and below.

The rotate() function accepts three parameters: the first one is the angle of rotation, the second and the third ones the coordinates of the center of rotation.

Once all the elements of the #cd-buildings group have been hidden, we focused on the #cd-loading. First, we wanted to hide the #cd-pause-btn. We assigned it a scale transformation:

<g id="cd-pause-btn" transform="translate(100 100) scale(0)">
   <!-- .... -->
</g>

One important note: the scale() function doesn't accept, as argument, the transform origin coordinates (the default one is the svg upper-left corner). To scale an object by a given factor around a point, you can combine two transformations:

transform="translate(-centerX(factor- 1) -centerY(factor- 1)) scale(factor)"

Since in our case factor=0 and (centerX, centerY) = (100, 100), we applied also a translate(100 100) transformation.

After that, we needed to animate the #cd-loading-circle-filled element (crimson circle which is drawn when you click the play button). To create this animation, we used the two svg attributes stroke-dasharray and stroke-dashoffset. Imagining the circle as a dashed line, the stroke-dasharray lets you specify dash length and gaps, while the stroke-dashoffset lets you change where the dasharray starts. In our case we set:

var loadingCircle = animatingSvg.select('#cd-loading-circle-filled'),
    circumf = Math.PI*(loadingCircle.attr('r')*2);

loadingCircle.attr({
   'stroke-dasharray': circumf+' '+circumf,
   'stroke-dashoffset': circumf,
});

This way, the dash and gap are both equal to the circle circumference, but we set stroke-dashoffset: circumf so that only the gap (transparent) is visible. To create the loading effect, we animated the stroke-dashoffset attribute:

var playBtn = animatingSvg.select('#cd-play-btn').
globalAnimation;

playBtn.click(function(){ 
   var strokeOffset = loadingCircle.attr('stroke-dashoffset').replace('px', '');
   //animate strokeOffeset desn't work with circle element - we need to use Snap.animate() rather than loadingCircle.animate()
   globalAnimation = Snap.animate(strokeOffset, '0', function( value ){ 
      loadingCircle.attr({ 'stroke-dashoffset': value })
   }, 1500, mina.easein);
});

One important note: the stroke-dashoffset attribute for a circle element cannot be animated using the animate() method directly (while it works fine for a path element); this is why we have been using the Snap.animate() method and stored the returned animation object in the globalAnimation variable in order to stop the animation when user clicks on the pause button:

var pauseBtn = animatingSvg.select('#cd-pause-btn');

pauseBtn.click(function(){
   //pause the animation on the loadingCircle
   globalAnimation.stop();
});

Once the circle animation is completed, we start animating the buildings and the clouds using the technique described at the beginning of this paragraph.

That's it! Hopefully browsers support will get better soon, and we will rely on SVG technology not only to create scalable graphic elements, but also complex animations ;)

Project duplicated

Project created

Globals imported

There was an error while trying to export your project. Please try again or contact us.