Advertisement

How to Animate Festive SVG Icons With CSS

by

'Tis the season, so in this tutorial, I'll walk through creating some CSS animated, holiday-themed, SVG icons. There are some great icons on Iconmelon, a site which hosts many free vector icon sets for you to sink your teeth into. The icons I'm using come courtesy of designer Sam Jones. So grab yourself a cup of eggnog, pull your laptop up to the yule log, and let's gets started!

SVG and the Web

If you're interested in using SVG on the web, icons are a great place to start. SVGs are flexible, resolution independent, and light weight, so icons naturally lend themselves to the vector format. Plus, just like HTML, SVGs can easily be styled with CSS, which includes CSS3 animation. Adding a dash of interactivity with animation to your icons can help create a delightful experience for your users and also add context about what an icon represents.

For an introduction on SVG and the web, take a look at SVG Files: From Illustrator to the Web.

Note: The following demos use cutting edge technologies that, as of the time of this writing, are not supported in some browsers such as Internet Explorer. If you're following along with the tutorial, it's best if you use Chrome or Safari. Mozilla support is perfectly possible with the appropriate property prefixes. You can certainly expect support for these technologies to improve in the future.

Additionally: In this article I've omitted some necessary browser prefixes from some CSS properties for concision and readability. Check out the prefixfree library by Lea Verou if you'd like to write prefix free CSS easily. You could also try creating your demos on Codepen, which can be easily configured to use prefixfree.


Preparing SVG Code for Editing

One of the biggest knocks on SVG is that the code is difficult to deal with. The SVG code that is exported by Illustrator, my vector graphics editor of choice, is pretty unreadable at first glance. Inkscape actually does a better job of exporting SVG in this respect, but I've found that simplifying and formatting the code can go a long way towards making the code easier to read and work with.

Here's the SVG code for the first animation example I'll demonstrate, a blinking Christmas light.

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.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" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
 width="64px" height="64px" viewBox="0 0 64 64" enable-background="new 0 0 64 64" xml:space="preserve">
<path id="outline" d="M38.178,12.142H25.822v10.723c-1.605,2.67-2.461,6.529-2.461,11.406c0,9.473,4.748,17.587,8.637,17.587
 s8.641-8.114,8.641-17.587c0-4.881-0.854-8.744-2.461-11.412V12.142z"/>
<rect id="basefill" x="29.822" y="16.142" fill="#4D4B4C" width="4.355" height="4.273"/>
<path id="lightfill" fill="#FFFFFF" d="M31.998,47.768c-1.402-0.959-4.637-6.229-4.637-13.497c0-5.775,1.271-8.566,2.207-9.855
 h4.857c0.945,1.293,2.213,4.08,2.213,9.855C36.639,41.542,33.402,46.809,31.998,47.768z"/>
</svg>

This code was exported from my vector editing tool, Adobe Illustrator.  It's almost unreadable at first glance.  Here's that same markup simplified:

<svg class="svg-light" 
	viewBox="0 0 64 64" 
	xmlns="http://www.w3.org/2000/svg"
	>

	<path class="outline"
		d="M38.178,12.142H25.823v10.723c-1.606,2.67-2.461,6.529-2.461,11.406c0,9.473,4.748,17.587,8.636,17.587c3.889,0,8.641-8.114,8.641-17.587c0-4.881-0.854-8.744-2.461-11.412V12.142z"
		/>

	<rect class="base"
		x="29.822"
		y="16.142"
		width="4.355"
		height="4.273"
	 	/>

	<path class="bulb"
		d="M31.998,47.768c-1.402-0.959-4.637-6.229-4.637-13.497c0-5.775,1.272-8.566,2.207-9.856h4.858c0.945,1.293,2.213,4.081,2.213,9.856C36.639,41.542,33.402,46.809,31.998,47.768"
	 	/>
</svg>

I've significantly simplified this xml by removing a lot of the extra markup that the program outputs by default. My base SVG element contains the following:

  • A class: svg-light. I've used the svg- prefix to easily target elements inside of a specific SVG.
  • The viewbox property.  The value of the viewbox property defines the aspect ratio of the document and is equivalent to the size of the artboard in Illustrator.
  • The xmnls property.  This property defines the XML namespace of the SVG and helps some user agents understand the markup.

To the elements nested inside the SVG which define the shapes that make up the image, such as the pathscircles and rects, I've applied classes inline with their opening tag. All inline properties of the element, such as their coordinates, I've broken to new lines. In addition, I've indented all of inner elements under the SVG base tag.

All this work is for a reason. First of all, it makes the code much easier to read. Second of all, in my code editor of choice, Sublime Text 3, I can easily fold individual SVG elements or entire SVGs while the class names still help retain context about what those elements are.

Code folding of SVG in Sublime Text 3
Code folding of SVG in Sublime Text 3

Christmas Lights

Alright, let's get into the holiday spirit and animate a blinking Christmas light! Here's what we'll be looking to achieve:

See the Pen Light by Noah Blon (@noahblon) on CodePen

What I want to do is animate the path element which I've given a class of bulb in the SVG.

<path class="bulb"
	d="M31.998,47.768c-1.402-0.959-4.637-6.229-4.637-13.497c0-5.775,1.272-8.566,2.207-9.856h4.858c0.945,1.293,2.213,4.081,2.213,9.856C36.639,41.542,33.402,46.809,31.998,47.768"
	/>

With CSS, I'll give the bulb a fill color and define its animation properties.

.svg-light .bulb {
    fill: hsl(204, 70%, 23%);
    animation-name: glow-blue;
    animation-duration: 1s;
    animation-iteration-count: infinite;
    animation-timing-function: ease-in-out;
    animation-direction: alternate;
}

The fill property allows us to set an SVG element's color. When possible, I like to use HSL (hue, saturation, lightness) to define color values because it is very intuitive to work with. Here, I've chosen a blue color (hue 204), and kept the lightness value low, 23%, meaning we'll have a dark blue color.

I've set the bulb to be animated by a keyframe animation called glow-blue. The duration of the animation is 1 second. The animation iterates infinitely, meaning it will run forever. The timing of the animation is eased at the beginning and end of the keyframes, creating a smoother looking transition at the start and end points of the animation. Finally, the animation is set to alternate, meaning it will go back and forth from the beginning to the end and back again.

Tip: You may know there is a shorthand syntax for CSS animation rules, but I generally prefer to split the rules out to make them easier to understand, modify, and share if chaining multiple animations.

Now, I'll define the glow-blue keyframe animation:

@keyframes glow-blue {
    0% { fill: hsl(204, 80%, 23%); }
    100% { fill: hsl(204, 80%, 63%); }
}

Here I've set the starting and and ending fill color of the element to which this animation is applied. The only value that is changing is the lightness of the color (the "l" bit in hsl), which means the light will animate from a darker to lighter version of the same hue. That's the intuitive syntax that HSL provides.

Because I've defined the animation to alternate infinitely, the light will alternate from dark to light and never stop. It will therefore appear to blink.

Animating multiple lights

OK, now let's channel our inner Clark W. Griswold and animate a whole load of Christmas lights. This is what I'll create:

See the Pen 540257e4a8b727e435c8e4033602ebb0 by Noah Blon (@noahblon) on CodePen

I've created five differently colored lights. Each light is wrapped in a div with a width of 20%. These divs are then floated left so they appear inline with each other. As I haven't set the SVG to be a set height or width and retained the viewbox property, the SVG is fluid to the size of its parent without losing its aspect ratio or quality. Just one of the huge strengths of SVG.

<svg class="svg-light svg-light--red">

For each SVG, I've extended the base svg-light class with a color suffix (for example svg-light--red). Here's an excerpt of the rules I've used to animate the many lights:

.svg-light .bulb {
	animation-duration: 1s;
	animation-iteration-count: infinite;
	animation-timing-function: ease-in-out;
	animation-direction: alternate;
}

.svg-light--red .bulb {
	fill: hsl(6, 63%, 16%);
	animation-name: glow-red;
	animation-delay: .5s;
	animation-duration: 1.25s;
}

@keyframes glow-red {
	0% { fill: hsl(6, 63%, 16%); }
	100% { fill: hsl(6, 63%, 56%); }
}

The basic animation properties are still applied to the base class of svg-light so that they can be shared between all of the lights. For each color variation, I've created a different keyframe animation such as glow-red or glow-blue and given them different animation delays and durations. In this way, the lights will blink with their appropriate color, and all not blink at the same time.


Rudolph the Red-Nosed Reindeer

I'll use the same concepts I've described above to animate the most famous reindeer of all, Rudolph. Here's the end result:

See the Pen SVG Rudolph Icon Animated with CSS by Noah Blon (@noahblon) on CodePen

First, I'll animate his glowing red nose:

.svg-rudolph .nose {
    animation-name: glow;
    animation-duration: 6s;
    animation-iteration-count: infinite;
    animation-timing-function: ease-in-out;
    animation-direction: alternate;
}

@keyframes glow {
    0% { fill: hsl(6, 93%, 16%); }
    50% { fill: hsl(6, 93%, 56%); }
    100% { fill: hsl(6, 93%, 56%); }
}

This looks pretty similar to our Christmas light. In the keyframe animation, I am animating to 50% and then to 100%. As the animation duration is six seconds, this means that in three seconds, the nose will become lighter red and remain that color for another three seconds to the end of the animation. Also, because I've set the animation to alternate, it will now run from the end to the beginning. Therefore the nose will stay full red for another three seconds before finally darkening in the last three seconds. Understanding the interplay between the keyframe percentages and the animation definitions, such as the duration, is key to creating effective CSS animations.

I've also applied a transition to the highlight on Rudy's nose:

.svg-rudolph .nose-highlight {
    fill-opacity: 0;
    animation-name: highlight-fade;
    animation-duration: 6s;
    animation-iteration-count: infinite;
    animation-timing-function: ease-in-out;
    animation-direction: alternate;
}

@keyframes highlight-fade {
    0% { fill-opacity: 0; }
    25% { fill-opacity: 0; }
    100% { fill-opacity: 1; }
}

Here I'm animating a different SVG property, the fill-opacity. This property takes a value between 0 and 1; 0 being fully transparent, 1 being fully opaque. Finally, I'll bring a bit more life to Rudy by making his eyes blink:

.svg-rudolph .eye {
    animation-name: blink;
    animation-duration: 8s;
    animation-iteration-count: infinite;
    transform-origin: 50%;
}

@keyframes blink {
    0% { transform: scaleX(1) scaleY(1); }
    1% { transform: scaleX(1.3) scaleY(0.1); }
    2% { transform: scaleX(1) scaleY(1); }
    60% { transform: scaleX(1) scaleY(1); }
    61% { transform: scaleX(1.3) scaleY(0.1);}
    62% { transform: scaleX(1) scaleY(1); }
    100% { transform: scaleX(1) scaleY(1); }
}

I've created an animation called blink. This animation runs for eight seconds and never stops. I'm not alternating the animation this time, instead the animation will run to the end and then restart from the beginning.

In the keyframes, the blinking animation is created by manipulating the scale of the eyes on the X and Y axes. For example, at the 1% mark, the eyes are scaled to 1.3 their size on the X axis and .1 times their normal size on the Y axis. In other words, they get a little bit wider and a lot shorter. One percent of the animation duration later, the scale of the eyes returns to normal. Therefore the scale change happens extremely quickly; 8 seconds / 100 = 8 hundredths of a second.

A key component of using transform to animate SVG elements is to set the elements transform-origin. In most cases, in order for a transform to be applied properly in an SVG, we must define the origin of the transform coordinate to be the center of the element. Unlike that of an HTML element, by default the transform origin of an SVG element is its top left corner, the (0,0) coordinate. By setting transform-origin to (50%, 50%), the origins for the X and Y axis of the transform will be at the center of the element, and transforms will then be applied correctly.

Note: Firefox has issues when it comes to transform origin. Unfortunately, setting the transform-origin to 50% actually resets the X/Y coordinates of the element. One work-around is to transform the element back to its original position.


Ding Dong Merrily on High

For this demo, I'll animate a bell ringing like this:

See the Pen 63cdc3e8e785028deb35132d889b6090 by Noah Blon (@noahblon) on CodePen

Let's take a look at an excerpt of the SVG markup:

	<svg class="svg-bell" viewBox="0 0 88 72" xmlns="http://www.w3.org/2000/svg">
		<g class="group-striker">
			<circle class="outline"
				cx="43.998" 
				cy="54.571" 
				r="7.99"
				/>
			<circle class="fill"
				cx="43.998" 
				cy="54.567" 
				r="3.99"
				/>
		</g>
		<g class="group-bell">
			<path class="outline"
				d="M71.5,38.184h-3.291l-6.213-21.879c-1.367-4.816-5.951-8.693-10.904-9.514C50.91,3.02,47.812,0,43.996,0c-3.812,0-6.91,3.018-7.092,6.787c-4.957,0.822-9.539,4.697-10.908,9.514l-6.211,21.883h-3.289l-4.498,15.85h19.251H36h15.994h3.963h20.041L71.5,38.184z M40.975,6.611C41.229,5.143,42.455,4,43.996,4c1.543,0,2.77,1.143,3.025,2.615L40.975,6.611z"
				/>
			<path class="fill"
				d="M29.844,17.395c1.045-3.678,5.156-6.783,8.975-6.783l10.355,0.004c3.82,0,7.93,3.105,8.975,6.783l5.902,20.785H23.943L29.844,17.395z"
				/>
			<polygon class="fill"
				points="19.52,42.184 68.477,42.184 70.705,50.033 17.291,50.033"
				/>
		</g>
	</svg>

I've wrapped the bell outline and fills with a <g> (or group) tag. This tag is very useful for organizing elements, similar to how you might wrap several HTML elements with a <div>. Animations can be applied to a group. Since in this case, I want both the fills and outline of the bell to rotate, I can just rotate the group rather than each element individually.

.svg-bell .group-bell {
    animation-name: bell-ring;
    animation-duration: 3s;
    animation-iteration-count: infinite;
    animation-timing-function: ease-in-out;
    animation-delay: -1.5s;
    animation-direction: alternate;
    transform-origin: 50%;
}

@keyframes bell-ring {
    0% { transform: rotate(27deg); }
    100% { transform: rotate(-27deg); }
}

This CSS should be familiar by now, but there are a couple of new things happening. First, in the keyframe animation, the bell is being transformed, but instead of transform scale I'm transforming the rotation value of the element. This animation makes the bell oscillate between 27 and -27 degrees.

The other new thing I've done is use a negative value for the animation-delay. A negative value causes the animation to start that amount of time into the animation. In this case, instead of the animation starting after a 1.5 second delay, the animation will actually start 1.5 seconds into the animation. By doing this, the bell will start at the neutral position, which it is at at 1.5 seconds into the animation, rather than at the rotated position, which it is at 0 seconds into the animation.

.svg-bell .striker {
    animation-name: striker-move;
    animation-duration: 3s;
    animation-iteration-count: infinite;
    animation-timing-function: linear;
    animation-delay: 1.5s;
    animation-direction: alternate;
    transform-origin: 50%;
}

@keyframes striker-move {
    0% { transform: translateX(3px); }
    100% { transform: translateX(-3px); }
}

I've applied a similar animation to the group containing the bell's striker elements. This time, however, I'm translating, or moving, the elements along the X-axis rather than rotating, so the striker will oscillate to the left and right.


Let It Snow! Let It Snow! Let It Snow!

For this animation, I'll animate snow falling inside a snow globe.

See the Pen 2addb5f98c757d61cec87bdcacb5870d by Noah Blon (@noahblon) on CodePen

I've created a bunch of circle elements to represent snow, and I've placed these elements around the perimeter of the snowglobe, wrapping them in a <g> tag.

The snow is not visible outside of the snowglobe because I've applied a clipping path to the group containing the snow. A clipping path is a shape that can be applied to SVG elements, masking others within it. In our globe example, I've created a circular clipping path which covers the interior of the globe.

<defs>
	<clipPath id="globeClipPath">
  		<circle cx="32" cy="31" r="26.825"/>
	</clipPath>
</defs>

The <clipPath> tag is our masking element, to effectuate it we must give it an ID.

I've wrapped the clipping path in a defs tag. SVG allows us to create elements which we can reuse later, such as clipping paths, filters, or shapes which we can stamp out. It is best practice to wrap definitions such as clipping paths in a defs tag so the SVG can be more easily read.

To apply the clipping path, we reference its url, in this case its ID preceded by a hash symbol, in the inline clipping path property of an SVG element:

<g class="group-snow" clip-path="url(#globeClipPath)">
    <circle class="snow" 
        cx="49.498" 
        cy="8.482" 
        r="2.5"
        />
    <circle class="snow" 
        cx="35.748" 
        y="2.783" 
        r="2.5"
        />
    <circle class="snow" 
        cx="21.001" 
        cy="4.728" 
        r="2.5"
        />
    <circle class="snow" 
        cx="28.25" 
        cy="2.783" 
        r="2.5"
        />
    <circle class="snow" 
        cx="42.997" 
        cy="4.728" 
        r="2.5"
        />
    <circle class="snow" 
        cx="14.502" 
        cy="8.482" 
        r="2.5"
        />
    <circle class="snow" 
        cx="9.194" 
        cy="13.793" 
        r="2.5"
        />
    <circle class="snow" 
        cx="54.806" 
        cy="13.793" 
        r="2.5"
        />
</g>

The snow is now only visible inside of that circular clipping path.

The snow is animated with the same techniques I've described earlier; translated along the Y-axis and the fill-opacity approaching zero as the animation ends.

.svg-snowglobe .snow { 
	animation-name: snowfall;
	animation-duration: 10s;
	animation-iteration-count: infinite;
	animation-timing-function: ease-in;
}

.svg-snowglobe .snow:nth-child(1) { animation-delay: 2s; }
.svg-snowglobe .snow:nth-child(2) { animation-delay: 4s; }
.svg-snowglobe .snow:nth-child(3) { animation-delay: 6s; }
.svg-snowglobe .snow:nth-child(4) { animation-delay: 8s; }
.svg-snowglobe .snow:nth-child(5) { animation-delay: 10s; }
.svg-snowglobe .snow:nth-child(6) { animation-delay: 12s; }
.svg-snowglobe .snow:nth-child(7) { animation-delay: 14s; }
.svg-snowglobe .snow:nth-child(8) { animation-delay: 16s; }
.svg-snowglobe .snow:nth-child(9) { animation-delay: 18s; }
.svg-snowglobe .snow:nth-child(10) { animation-delay: 20s; }

Yes, CSS3 selectors work on SVG elements! Here, I've given each snow element a different delay so they won't all fall at once.


Season's Greetings

Finally, I'll animate some SVG-based text to achieve a result like this:

See the Pen Message by Noah Blon (@noahblon) on CodePen

SVG provides us with the ability to apply strokes to SVG elements with CSS. For my last animation, I'll show you how to progressively draw an element's stroke.

To achieve this animation, I've used the following stroke properties:

  • stroke-width: the stroke's width. This is relative to the size of the SVG and therefore responsive.
  • stroke: the stroke's color.
  • stroke-dasharray: defines a dashed stroke. An array of alternating values define the length of the drawn parts of the dashed line and the empty space between dashes.
  • stroke-dashoffset: defines where the stroke starts in relation to the length of the path.

Setting up the stroked text requires some work in a graphics editor. My workflow is as follows:

  1. Create a text element.
  2. Convert that text to vector paths.
  3. Merge those paths into a compound path.

Here's my CSS to set up the animation:

.svg-message .text {
    stroke-width: 1px;
    stroke: hsl(6, 63%, 36%);
    stroke-dasharray: 1865.753px 1865.753px;
    stroke-dashoffset:  1865.753px ;
    fill-opacity: 0;
    fill: hsl(6, 63%, 36%);
    animation-name: stroke, fill;
    animation-duration: 1s;
    animation-delay: 0, 1s;
    animation-iteration-count: 1;
    animation-timing-function: ease-in-out;
    animation-fill-mode: forwards;
}

@keyframes fadeIn {
    0% { fill-opacity: 0; }
    100% { fill-opacity: 1; }
}

@keyframes drawStroke {
    100% { stroke-dashoffset: 0 }
}

I've defined the stroke-dasharray property to be the length of the total path (you can find this value in Illustrator under the Document Info Palette using the Object option). Then, I've given the stroke-dashoffset a value of the total length of the path, which pushes the entire stroke out of visibility. Then, by keyframe animating the stroke-dashoffset property, the stroke will progressively draw on screen.

If you want to learn more about this technique, check out Jake Archibald's post Animated line drawing in SVG.

I've also set the text to fade in after the line drawing has completed, using the fill-opacity property similar to what I did earlier with the snowglobe animation.


Going Further with SVG and CSS

In this tutorial, I've demonstrated that with CSS and SVG, you can create some pretty effective animations. If you want to get stuck in with all of the code, as well as some additional animations, you can download the examples from Github or checkout my collection on Codepen.

If you want to get more advanced, you could explore controlling CSS animations with JavaScript or supplementing your CSS animations with more complex JavaScript animation, such as that supported by the amazing SVG JS library Snap SVG.

Thanks for following along with this tutorial, I can't wait to see the creations you come up with!