Scalable Vector Graphic, or SVG, is an image format we utilise as much as possible in our projects. An SVG’s small file size, in addition to its ability to scale (clue’s in the name, there!) wonderfully to any size makes it a fantastic tool for the web. If you’re happy to fall back to less interesting alternatives (or include polyfills) for IE users, there are many great things you can do with SVG.
NB: SVGs aren’t viewable in < IE9, so if you’re on IE8 (or worse), you won’t be able to view the SVG at its various stages of development. Use a better browser, or Chrome Frame if you absolutely have to use IE.
Normally, we would export an SVG from Adobe Illustrator, include it within our project, and perform any necessary animations with external JavaScript or CSS. When I decided to do some further research on SVG, I realised that there were other options available. I have begun to devour the spec itself, rather than learning one of the several JavaScript libraries available, as I like to know exactly what’s happening and why. I’ve only just begun to scratch the surface of SVGs capabilities, but I thought I’d share some of what I’ve learned so far.
Many sites use an icon of three horizontal lines to depict a concealed menu, whether it be solely for mobile users, or to provide more room for a larger content area. Whatever the reason, it’s a popular feature at the minute. On the majority of sites I’ve seen that have this feature, the menu will animate into appearance when the icon is clicked, but the icon itself won’t actually do anything. Why don’t we, as an example, say that we want the menu icon to perform an animation, too — such that it will rotate on a click event — presenting as vertical when the menu is open, and horizontal when closed. Sounds easy, right? Just throw some jQuery at it, and hey presto, job done. But why use jQuery when it’s not necessary? It seems silly to rely upon jQuery — or even plain JavaScript — animations for SVGs, when the same effect can be accomplished by leveraging the power of SVG itself. Keeping the animation for the menu separate from that of the menu icon can be advantageous, especially if the main animation isn’t unique to the menu itself, but is also used in other areas throughout the site.
Now, when I was thinking about this, I hadn’t previously looked at click event animations; I had written some for hover
events, but this was a different thing entirely. With a hover
event (or mousein
/mouseout
), one can either write a separate animation for each action, or just a single one that can be reversed. With a click event, simply clicking the animation target will cause the animation to restart. At this point, you might think that it makes more sense to just use external JS/jQuery, but resolving this is actually rather straightforward. I’ve recreated the steps I took, as follows.
Step one — cleaning up the markup
If you’re going to be working directly with the XML markup of the SVG, it’s worth taking some time to clean up the code outputted by Illustrator, Sketch, or other program that you’ve exported the code from, or even writing it from scratch yourself. Neither Illustrator nor Sketch output great code and, depending on how you’ve created your SVG, you may end up with a ridiculous amount of bloat. In our example, a simple menu icon — consisting of nothing more than three rectangles — neither program generated genuine <rect>
tags. Rather, they outputted <path>
tags. This isn’t necessarily the programs’ fault, but we can simplify the code quite easily. Here’s the full SVG output from each:
Illustrator’s output:
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 17.0.2, 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="40px" height="35px"
viewBox="0 0 40 35" enable-background="new 0 0 40 35" xml:space="preserve">
<g>
<g transform="translate(0.000000, 4.000000)">
<path fill="none" stroke="#444444" stroke-width="9" stroke-miterlimit="10" d="M0,26.496h40"/>
<path fill="none" stroke="#444444" stroke-width="9" stroke-miterlimit="10" d="M0,13.496h40"/>
<path fill="none" stroke="#444444" stroke-width="9" stroke-miterlimit="10" d="M0,0.5h40"/>
</g>
</g>
</svg>
Sketch’s output:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="40px" height="35px" viewBox="0 0 40 35" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<title>Slice 1</title>
<description>Created with Sketch (http://www.bohemiancoding.com/sketch)</description>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<path d="M0,4.5 L40,4.5" id="Shape_2_" stroke="#444444" stroke-width="9" sketch:type="MSShapeGroup"></path>
<path d="M0,17.496 L40,17.496" id="Shape_1_" stroke="#444444" stroke-width="9" sketch:type="MSShapeGroup"></path>
<path d="M0,30.496 L40,30.496" id="Shape" stroke="#444444" stroke-width="9" sketch:type="MSShapeGroup"></path>
</g>
</svg>
Simplified code:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg width="40px" height="40px" viewBox="0 0 40 40" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
<g stroke="none" fill="#444">
<rect x="0" y="0" width="40" height="9" />
<rect x="0" y="15.5" width="40" height="9" />
<rect x="0" y="31" width="40" height="9" />
</g>
</svg>
I found it quite interesting to see the differences between the two pieces of software in how they generate the paths: Sketch — for whatever reason — clears the fill colour for the group, before overwriting it on every element within. I think that the simplified code looks a lot tidier & more readable than the output from either app. Obviously with more complex SVGs, it would be a much bigger task to (re-)code the entire SVG by hand; it may not always be practical, but I find the process to be quite therapeutic.
Step two — creating the animation
Rather than worry about the click event from the word go, I felt it best to get the animation done first. There are several different elements (based on the SMIL spec) available for performing actions:
These elements will perform the animation upon whichever element it is wrapped within, provided that its parent is animatable. <set>
is slightly different, but that is outside the scope of this post (check out its properties in the link). Since we’re performing a transformation — a rotation, specifically — I opted for <animateTransform>
. There are several attributes we have to set to enable our required transformation: the type of transformation, the start and end points of our animation, duration, and the repeat mode. Most importantly, we also need to specify the event that will trigger the element’s action.
So, from this:
<animateTransform begin="begin-value-list"
attributeName="<attributeName>"
type="translate | scale | rotate | skewX | skewY"
dur="Clock-value | media | indefinite"
from="<tx> [,<ty>] | <sx> [,<sy>] | <rotate-angle> [<cx> <cy>] | <skew-angle> | <skew-angle>"
to="<tx> [,<ty>] | <sx> [,<sy>] | <rotate-angle> [<cx> <cy>] | <skew-angle> | <skew-angle>"
fill="freeze | remove" />
we get this:
<animateTransform begin="click" attributeName="transform" type="rotate" dur="0.5s" from="0 20 20" to="-90 20 20" fill="freeze" />
I think the first four attributes are self-explanatory, so let me focus on the last three — from
, to
, and fill
. The first attribute value in from
/to
refers to the rotation angle; so, you can see that we want to rotate from 0deg to -90deg (negative to indicate anti-clockwise direction). The second value refers to the x-axis centre-point for the rotation, and the third for the y-axis centre-point. Since our SVG is 40px * 40px, our centre-point is 40/2 = 20px for each axis. The final attribute — fill
— specifies what we want to happen upon animation completion. We have two choices:
1. Freeze the animation target as it appears once the animation has completed.
2. Remove the result of the animation. Essentially, revert the target to its original state.
We’re using “freeze” here, since we want the orientation to remain vertical until the user initiates a second click. Here‘s what we have so far.
Step three – building upon what we have
As you can see, there are a few issues with this. Firstly, the lines are clipped during the animation. This is because the elements are partially traversing outside of the viewBox
, which is the viewable area of the SVG. Secondly, the animation will only trigger if one of the lines is clicked; if the whitespace is clicked, nothing happens. Thirdly, although the animation does what we want it to on the first click, any subsequent clicks restart the animation, rather than reversing it.
The viewBox problem
The easiest way to solve this is to increase the size of the viewBox. However, if we increase the viewBox without translating that to the elements within, it will look skewif. So, I’ll make the viewBox 20px wider and higher, and move the elements +10px on both axis. I’ll also have to change the rotation centre-points, so that the rotation originates from the very centre of the viewBox. Thus, we’ll end up with this:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg width="60px" height="60px" viewBox="0 0 60 60" version="1.1"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve">
<g stroke="none" fill="#444">
<rect x="10" y="10" width="40" height="9" />
<rect x="10" y="25.5" width="40" height="9" />
<rect x="10" y="41" width="40" height="9" />
<animateTransform begin="click" attributeName="transform" type="rotate" dur="0.5s" from="0 30 30" to="-90 30 30" fill="freeze" />
</g>
</svg>
The whitespace problem
There’s a fairly simple solution to this issue, too: we just need to include a box inside of our group; one that fills the whole SVG viewport. That way, even when seemingly empty space is clicked, the event is still hitting an element. The one hurdle that we encounter is that we need to specify a fill colour, otherwise the element may as well not exist. However, if we specify a visible colour, we’ll have to edit the SVG every time we want to change the background colour of the element we’re going to sit this SVG on. It’s not much of a hurdle, though, as we can assign
fill="transparent | rgba(0,0,0,0)"
The tag for a box is just <rect>
. We specify its origin point on x- and y-axis, its width and height, and that’s about it!
<rect x="0" y="0" width="60" height="60" fill="transparent" stroke="none" stroke-width="0" />
I’ve set the stroke and stroke-width values here just to be safe. We include this in our group (before or after the lines, it doesn’t matter), and away we go!
The click event problem
Now, this is where it gets a little more difficult. We’ve successfully triggered the -90deg rotation on the first click, and now we want to trigger a rotation back to 0deg on a second click, but there is only one type of click event. We can’t check for state, or whether the group has already been clicked, so how can we trigger the return animation without triggering the first?
The answer is ECMAScript. If you’ve not heard of ECMAScript, it’s the scripting language most commonly implemented as JavaScript. So ‘why not use JavaScript?’, I hear you ask. There’s nothing stopping you from declaring JavaScript, but the default MIME type is ECMAScript, so I stuck with that for this example.
I looked at several different solutions to this issue, but settled on the following.
<script type="application/ecmascript">
<![CDATA[
function menu(evt) {
var trigger = document.getElementById("trigger");
if(typeof clicked === 'undefined') {
clicked = true;
trigger.setAttribute("from", "0 30 30");
trigger.setAttribute("to", "-90 30 30");
} else {
delete clicked;
trigger.setAttribute("from", "-90 30 30");
trigger.setAttribute("to", "0 30 30");
}
}
]]>
</script>
This function is triggered when the SVG element is clicked (using the onactivate
attribute on the SVG element to trigger it). On the first click, the variable clicked
is undefined, so the first part of the if statement is executed. We define the clicked
variable (in preparation for the second click), and invert the from
& to
attributes of the <animateTransform>
element (#trigger
). Therefore, when the SVG is clicked for a second time, the animation triggered is the inverse of the original; this will also execute the second part of the if statement, which deletes the clicked
variable, and reverts the animation to its original state.
And that’s it! Here’s the finished SVG:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<svg onactivate="menu(evt)" width="60px" height="60px" viewBox="0 0 60 60" version="1.1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xml:space="preserve">
<script type="application/ecmascript">
<![CDATA[
function menu(evt) {
var trigger = document.getElementById("trigger");
if(typeof clicked === 'undefined') {
clicked = true;
trigger.setAttribute("from", "0 30 30");
trigger.setAttribute("to", "-90 30 30");
} else {
delete clicked;
trigger.setAttribute("from", "-90 30 30");
trigger.setAttribute("to", "0 30 30");
}
}
]]>
</script>
<g stroke="none" fill="#444">
<rect x="10" y="10" width="40" height="9" />
<rect x="10" y="25.5" width="40" height="9" />
<rect x="10" y="41" width="40" height="9" />
<rect x="0" y="0" width="60" height="60" fill="transparent" stroke="none" stroke-width="0" />
<animateTransform id="trigger" begin="click"
attributeName="transform" type="rotate" additive="replace"
from="0 30 30" to="-90 30 30" dur="0.5s" fill="freeze" />
</g>
</svg>
Limitations
As with everything, there are limitations to SVG. In this instance, we not only wanted to activate the animation on click, but we also wanted to tie it to the menu the icon represents. Unfortunately, this ruled out embedding the SVG in an img
tag, since both scripts and event-based SVG animations are disabled for that method. It also ruled out injection via an object
/iframe
/embed
tag, due to the separate contexts (as in, we’d not be able to maintain disparate DOM manipulation). We could leave the triggers separate, allowing the main JavaScript to handle the menu animation on its own click event, and hope that both events fire simultaneously at all times, or we could embed the SVG code directly into the HTML, and use a single click event to trigger both functions. See this nice comparison of functionality between all the different methods of injection for further information.
There is another way of triggering both animations from one event, but it ties (albeit loosely) the SVG to this use case. It is possible to call functions on the parent window from within the SVG; so, by binding the menu animation function, as var fnName = function() {};
, to the window with window.fnName = fnName;
in the JavaScript file, it can be called within the SVG function with window.parent.fnName();
. Works like a charm but, as you can see, the SVG will now need editing if the JavaScript were ever altered, or if it was to be used in a different scenario. If this were merely a timer-based animation, there would be no issue with using an img tag. Similarly, if there was no requirement for event triggers outside of the SVG context (or you were happy listening for click events in two places), there would be no reason why one could not inject the SVG via an object
/iframe
/embed
tag. A few things to bear in mind when creating SVG animations.
In closing
This is a really simple SVG, but it should give you an inkling on the power of the technology; there are many more complex and interesting things possible with it. This is only the first post in a series on the topic of SVGs so, if you found this interesting, check back for more! I’m primarily going to be focusing on animation but, if you’d like me to cover something else, just drop me an email, or hunt me down on Twitter 🙂
UPDATE (29/11): I’ve been informed that the SVG, as it stands, doesn’t work properly in FireFox (see this comment on Reddit). This is because we’re currently using the onactivate
attribute on the SVG element to trigger the function, and FireFox seemingly doesn’t understand it properly. Changing this to onclick
fixes that, as per this updated fiddle.