Easy SVG Customization And Animation: A Practical Guide
Scalable Vector Graphics (SVG) have been a staple in Web Development for quite some time, and for a good reason. They can be scaled up or down without loss of quality due to their vector properties. They can be compressed and optimized due to the XML format. They can also be easily edited, styled, animated, and changed programmatically.
At the end of the day, SVG is a markup language. And just as we can use CSS and JavaScript to enhance our HTML, we can use them the same on SVGs. We could add character and flourishes to our graphic elements, add interactions, and shape truly delightful and memorable user experiences. This optional but crucial detail is often overlooked when building projects, so SVGs end up somewhat underutilized beyond their basic graphical use cases.
How can we even utilize SVGs beyond just using them statically in our projects?
Take the “The State of CSS 2021” landing page, for example. This SVG Logo has been beautifully designed and animated by Christopher Kirk-Nielsen. Although this logo would have looked alright just as a static image, it wouldn’t have had as much of an impact and drawn attention without this intricate animation.
Let’s go even further — SVG, HTML, CSS, and JavaScript can be combined and used to create delightful, interactive, and stunning projects. Check out Sarah Drasner’s incredible work. She has also written a book and has a video course on the topic.
Let’s add it to our HTML and create a simple button component.
<button type="button">
<svg width="24" height="24" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="..." fill="#C2CCDE" /></svg>
Add to favorites
</button>
Our button already has some background and text color styles applied to it so let’s see what happens when we add our SVG star icon to it.
Our SVG icon has a fill
property applied to it, more specifically, a fill="#C2CCDE"
in SVG’s path
element. This icon could have come from the SVG library or even exported from a design file, so it makes sense for a color to be exported alongside other graphical properties.
SVG elements can be targeted by CSS like any HTML element, so developers usually reach for the CSS and override the fill
color.
.button svg * {
fill: var(--color-text);
}
However, this is not an ideal solution as this is a greedy selector, and overriding the fill
attribute on all elements can have unintended consequences, depending on the SVG markup. Also, fill
is not the only property that affects the element’s color.
Let’s showcase this downside by creating a new button and adding a Google logo icon. SVG markup is a bit more complex than our star icon, as it has multiple path
elements. SVG elements don’t have to be all visible, there are cases when we want to use them in different ways (as a clipping region, for example), but we won’t go into that. Just keep in mind that greedy selectors that target SVG elements and override their fill
properties can produce unexpected results.
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24">
<path d="..." fill="#4285F4" />
<path d="..." fill="#34A853" />
<path d="..." fill="#FBBC05" />
<path d="..." fill="#EA4335" />
<path d="..." fill="none" />
</svg>
We can look at the issue from a different perspective. Instead of looking for a silver bullet CSS solution, we can simply edit our SVG. We already know that the fill
property affects the SVG element’s color so let’s see what we can do to make our icons more customizable.
Let’s use a very underutilized CSS value: currentColor
. I’ve talked about this awesome value in one of my previous articles.
Often referred to as “the first CSS variable,”currentColor
is a value equal to the element’scolor
property. It can be used to assign a value equal to the value of thecolor
property to any CSS property which accepts a color value. It forces a CSS property to inherit the value of thecolor
property.
If you are looking for more, CSS-Tricks keeps a comprehensive list of various SVG optimization tools with plenty of information and articles on the topic.
Using SVGs With Popular JavaScript-Based Frameworks
Many popular JavaScript frameworks like React have fully integrated SVG in their toolchains to make the developer experience easier. In React, this could be as simple as importing the SVG as a component, and the toolkit would do all the heavy lifting optimizing it.
import React from 'react';
import {ReactComponent as ReactLogo} from './logo.svg';
const App = () => {
return (
<div className="App">
<ReactLogo />
</div>
);
}
export default App;
However, as Jason Miller and many other developers have noted, including the SVG markup in JSX bloats the JavaScript bundle and makes the SVG less performant as a result. Instead of just having the browser parse and render an SVG, with JSX, we have expensive extra steps added to the browser. Remember, JavaScript is the most expensive Web resource, and by injecting SVG markup into JSX, we’ve made SVG as expensive as well.
One solution would be to create SVG symbol objects and include them with SVG use. That way, we’ll be defining the SVG icon library in HTML, and we can instantiate it and customize it in React as much as we need to.
<!-- Definition -->
<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<symbol id="myIcon" width="24" height="24" viewBox="0 0 24 24">
<!-- ... -->
</symbol>
<!-- ... -->
</svg>
<!-- Usage -->
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<use href="#myIcon" />
</svg>
Breathing Life Into SVGs
Animating SVGs can be easy and fun. It takes just a few minutes to create some simple and effective animations and interactions. If you are unsure which animation would be ideal for a graphic or should you animate it at all, it’s best to consult with the designer. You can even look for some similar examples and use cases on Dribble or other similar websites.
It’s also important to keep in mind that animations should be tasteful, add to the overall user experience, and serve some purpose (draw the user’s attention, for example).
We’ll cover various use cases that you might encounter on your projects. Let’s start with a really sweet example.
Animating A Cookie Banner
Some years ago, I was working on a project where a designer made an adorable cookie graphic for an unobtrusive cookie consent popup to make the element more prominent. This cookie graphic was whimsical and a bit different from the general design of the website.
I’ve created the element and added the graphic, but when looking at the page as a whole, it felt kind of lifeless, and it didn’t stand out as much as we thought it will. The user needed to accept cookies as the majority of website functionality depended on cookies. We wanted to create an unobtrusive banner that doesn’t block user navigation from the outset, so I decided to animate it to make it more prominent and add a bit of flourish and character.
I’ve decided to create three animations that’ll be applied to the cookie SVG:
- Quick and snappy rolling fade-in entry animation;
- Repeated wiggle animation with a good amount of delay in between;
- Repeating and subtle eye sparkle animation.
Here’s the final result of the element that we’ll be creating. We’ll cover each animation step by step.
Let’s store it in a CSS variable so that we can reuse it for the repeatable wiggle movement animation.
--transition-bounce: cubic-bezier(0.2, 0.7, 0.4, 1.65);
Let’s put everything together, set a duration value and fill-mode, and add the animation to our svg
element.
/* Our SVG element */
.cookie-notice__graphic {
opacity: 0; /* Should not be visible at the start */
animation: enter 0.8s var(--transition-bounce) forwards;
}
Let’s check out what we’ve created. It already looks really nice. Notice how the bouncing easing function made a lot of difference to the overall look and feel of the whole element.
@keyframes wiggle {
/* Stands still */
0% {
transform: translate3d(0, 0, 0) rotateZ(17deg);
}
/* Starts moving */
45% {
transform: translate3d(0, 0, 0) rotateZ(17deg);
}
/* Pulls back */
50% {
transform: translate3d(-10%, 0, 0) rotateZ(8deg);
}
/* Moves forward */
55% {
transform: translate3d(6%, 0, 0) rotateZ(24deg);
}
/* Returns to starting position */
60% {
transform: translate3d(0, 0, 0) rotateZ(17deg);
}
/* Stands still */
100% {
transform: translate3d(0, 0, 0) rotateZ(17deg);
}
}
/* Our SVG element */
.cookie-notice__graphic {
opacity: 0;
animation: enter 0.8s var(--transition-bounce) forwards,
wiggle 6s 3s var(--transition-bounce) infinite;
}
SVG elements can have a CSS class
attribute, so we’ll use that to target them. Let’s add the class
attribute to the two path
elements that we identified.
<!-- ... -->
<path fill="#351f17" d="..." />
<path class="cookie__eye" fill="#fff" d="..." />
<path fill="#351f17" d="..." />
<path class="cookie__eye" fill="#fff" d="..." />
<!-- ... -->
We want to make cookie’s eyes sparkle. I got this idea from a music video for a song by Devin Townsend. You can see the animation play at the 5-minute mark. It just goes to show how you can find ideas pretty much anywhere.
Let’s just change the scale and opacity. Notice how so far, we’ve relied only on those two attributes for all three animations, which are quite different from each other.
@keyframes sparkle {
from {
opacity: 0.95;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
We want this animation to repeat without delay. It should be subtle enough to blend in nicely with the graphic and the overall element and not obtrusive for the user. As for the easing function, we’ll do something different. We’ll use staircase functions to achieve that quick and snappy transition between the two animation states (our from
and to
values).
We need to be careful here. Transform origin is going to be set relative to the parent SVG element’s viewbox
and not the element itself. So if we set transform-origin: center center
, the transformation will use the center coordinates of the parent SVG and not the path
element. We can easily fix that by setting a transform-box property to fill-box
.
The nearest SVG viewport is used as the reference box. If aviewBox
attribute is specified for the SVG viewport creating element, the reference box is positioned at the origin of the coordinate system established by theviewBox
attribute, and the dimension of the reference box is set to the width and height values of theviewBox
attribute.
.cookie__eye {
animation: sparkle 0.15s 1s steps(2, jump-none) infinite alternate;
transform-box: fill-box;
transform-origin: center center;
}
Last but not least, let’s respect the user’s accessibility preferences and turn off all animations if they have it set.
@media (prefers-reduced-motion: reduce) {
*,
::before,
::after {
animation-delay: -1ms !important;
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
background-attachment: initial !important;
scroll-behavior: auto !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}
}
Here is the final result. Feel free to play around with the demo and experiment with keyframe values and easing values to change the look and feel of the animation.
Let’s take a closer look at the SVG we’ll be working with. It consists of a few dozen circle
elements.
<!-- ... -->
<circle cx="103.5" cy="34.5" r="11.3"></circle>
<circle cx="172.5" cy="34.5" r="15.7"></circle>
<circle cx="310.5" cy="34.5" r="24.6"></circle>
<circle cx="517.5" cy="34.5" r="34.5"></circle>
<circle cx="586.5" cy="34.5" r="34.5"></circle>
<circle cx="655.5" cy="34.5" r="33.4"></circle>
<!-- ... -->
Let’s start by adding a bit of opacity to our background and making it more chaotic. When we apply CSS transforms to elements inside SVG, they are transformed relative to the SVG’s main viewbox. That is why we’re getting a slightly chaotic displacement when applying a scale
transform. We’ll use that to our advantage and not change the reference box.
To make things a little bit easier for us, we’ll use SASS. If you are unfamiliar with SASS and SCSS, you can view compiled CSS in CodePen below.
svg circle {
opacity: 0.85;
&:nth-child(2n) {
transform: scale3d(0.75, 0.75, 0.75);
opacity: 0.3;
}
With that in mind, let’s add some keyframes. We’ll use two sets of keyframes that we’ll apply randomly to our circle elements. Once again, we’ll leverage the scale
transform displacement and change the opacity value.
@keyframes a {
0% {
opacity: 0.8;
transform: scale3d(1, 1, 1);
}
100% {
opacity: 0.3;
transform: scale3d(0.75, 0.75, 0.75);
}
}
@keyframes b {
0% {
transform: scale3d(0.75, 0.75 0.75);
opacity: 0.3;
}
100% {
opacity: 0.8;
transform: scale3d(1, 1, 1);
}
}
Now, let’s use quite a few :nth-child
selectors. Every odd child will use the a
keyframes, while every even circle will use a b
keyframes. We’ll use :nth-child
selectors to play around with animation duration and animation delay values.
svg circle {
opacity: 0.85;
animation: a 10s cubic-bezier(0.45,0.05,0.55,0.95) alternate infinite;
&:nth-child(2n) {
transform: scale3d(0.75, 0.75, 0.75);
opacity: 0.3;
animation-name: b;
animation-duration: 6s;
animation-delay: 0.5s;
}
&:nth-child(3n) {
animation-duration: 4s;
animation-delay: 0.25s;
}
/* ... */
}
And, once again, just by playing around with opacity values and CSS transforms on our SVG and playing around with child selectors and animation parameters, we’ve managed to create a more interesting background for our hero container.
Here is a markup for our circle SVG.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="50" fill-opacity=".03"/></svg>
Be careful not to inline too much data with base64, so stylesheets can be downloaded and parsed quickly. When we convert it to base64, we get this handy CSS background-image
snippet:
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48Y2lyY2xlIGN4PSI1MCIgY3k9IjUwIiByPSI1MCIgZmlsbC1vcGFjaXR5PSIuMDMiLz48L3N2Zz4=);
We can simply apply a simple animation where we offset the background-position
by the background-size
value and get this neat background animation.
.wrapper {
animation: move-background 3.5s linear;
background-image: url(data:image/svg+xml;base64,...);
background-size: 96px;
background-color: #16a757;
/* ... */
}
@keyframes move-background {
from {
background-position: 0 0;
}
to {
background-position: 96px 0;
}
}
Our example looks more interesting with this subtle moving animation going on in the background. Remember to respect users’ accessibility preferences and turn off the animations if they have a preference set.
Before diving into the animation, we need to cover two SVG properties that we’ll be using: stroke-dasharray
and stroke-dashoffset
. They’re integral for pulling off this animation.
Stroke can be converted to dashes with a certain length using a stroke-dasharray
property.
And we can offset the positions of those strokes by a certain amount using the stroke-dashoffset
property.
So, what’s this have to do with our drawing and erasing animation? Imagine what would happen if we could have a dash that covers the whole stroke length and offset it by the same value. In that case, the starting point of the stroke would be way past the ending point of the stroke, and we wouldn’t see it.
svg path {
stroke-linecap: round;
stroke-linejoin: round;
stroke-dasharray: 800; /* Dash covering the whole stroke */
stroke-dashoffset: 800; /* Offset it to make it invisible */
}
If we animate the offset value from that value back to 0, the stroke would slowly become visible, as it was drawing itself.
svg path {
/* ... */
animation: draw 6s linear infinite;
}
@keyframes draw{
to {
stroke-dashoffset: 0; /* Reduce offset to make it visible */
}
}
If we continue to animate the offset value from 0 to a negative value, we’d get the erasing effect.
svg path {
/* ... */
animation: drawAndErase 6s linear infinite;
}
@keyframes drawAndErase {
to {
stroke-dashoffset: -800;
}
}
You’re probably wondering where the magical 800
pixel value came from. This value depends on the SVG and the length of the dash needed to cover the whole stroke length. It can be easily guessed, but Chris Coyier has a handy function that can do it for you. However, depending on the stroke properties and SVG shape, this function might not always return an ideal value, but it can guide you closer to it.
Check out the complete demo and feel free to play around with values to see how the stroke properties affect the animation. If you are looking for more examples, CodyHouse has covered a fun-looking button animation using the same trick.
Let’s start by adding the mouse-tracking eye animation. We’ll skip manually implementing this feature in JavaScript and use a handy library called watching-you.
Using the browser’s inspect element tool, we’ll find the target elements inside the SVG and add the eye-left and eye-right CSS classes to these elements, respectively.
<ellipse class="cls-5 eye eye-left" cx="245.15133" cy="134.57033" rx="5.31264" ry="8.61816" transform="translate(-33.47349 110.5587) rotate(-23.83807)" />
<ellipse class="cls-4 eye eye-right" cx="284.42686" cy="116.68559" rx="5.31264" ry="8.61816" transform="translate(-22.89477 124.9063) rotate(-23.83807)" />
We’ll configure the library and make it target the classes that we’ve added.
const optionsLeft = { power: 4, rotatable: false };
const watcherLeft = new WatchingYou(".eye-left", optionsLeft);
watcherLeft.start();
const optionsRight = { power: 3, rotatable: false };
const watcherRight = new WatchingYou(".eye-right", optionsRight);
watcherRight.start();
We also need to remember to apply the transform-box
property, so our eyes move around the center.
.eye {
transform-box: fill-box;
transform-origin: center center;
}
Let’s check out what we’ve got. With just a few lines of code and a tiny JavaScript library to do the heavy lifting, we’ve made the SVG element respond to the mouse position. Now that’s amazing, isn’t it?
Bowtie and hat animation will be created in a very similar way. Let’s start with a hat and find it using the browser’s inspect element tool. The hat graphic consists of two path elements, so let’s group them.
<g class="hat">
<path class="cls-6" d="..." />
<path class="cls-9" d="..." />
</g>
We’ll apply the same transform-box
property and add a hat--active
class that will run the animation when applied.
.hat {
transform-box: fill-box;
transform-origin: center bottom;
cursor: pointer;
}
.hat--active {
animation: hatJump 1s cubic-bezier(0, 0.7, 0.5, 1.25);
}
@keyframes hatJump {
0% {
transform: rotateZ(0) translateY(0);
}
50% {
transform: rotateZ(-10deg) translateY(-50%);
}
100% {
transform: rotateZ(0) translateY(0);
}
}
Finally, let’s set up a click event listener that applies an active class to the element and then removes it after the animation has finished running.
const hat = document.querySelector(".hat");
hat.addEventListener("click", function () {
if (hat.classList.contains("hat--active")) {
return;
}
// Add the active class.
hat.classList.add("hat--active");
// Remove the active class after 1.2s.
setTimeout(function () {
hat.classList.remove("hat--active");
}, 1200);
});
We use the same trick with the bowtie element, only applying a different animation and class. Feel free to check out the CodePen demo for more details.
Let’s move on to the coffee machine. Notice we don’t have any SVG element acting as a coffee on our SVG, so we’ll need to add it ourselves. You should feel comfortable editing SVG markup and we don’t even have to break a sweat here. Let’s make it easy for ourselves and find and copy the coffee machine’s pipe rectangle, which is similar to the coffee stream shape we want to have. We just have to change the color to brown and slightly adjust the dimensions.
<!-- Pipe -->
<rect class="cls-12" x="137.81171" y="243.99883" width="6.21967" height="12.29272" transform="translate(281.84309 500.29037) rotate(-180)" />
<!-- Copied and adjusted Pipe rect to act as a coffee -->
<rect class="coffee" x="139" y="243.99883" width="4" height="12.29272" transform="translate(281.84309 500.29037) rotate(-180)" fill="brown" />
Like in the previous examples, let’s add active classes and their respective animation keyframes. We’ll compose the two animations and play around with duration and delay.
.lever, .coffee {
transform-box: fill-box;
transform-origin: center bottom;
}
.lever {
cursor: pointer;
}
.lever--active {
animation: leverPush 2.5s linear;
}
@keyframes leverPush {
0% {
transform: translateY(0);
}
8% {
transform: translateY(50%);
}
90% {
transform: translateY(50%);
}
100% {
transform: translateY(0);
}
}
.coffee--active {
animation: coffeeStream 2.4s 0.1s ease-out forwards;
}
@keyframes coffeeStream {
0% {
transform: translateY(0);
}
5% {
transform: translateY(50%);
}
95% {
transform: translateY(50%);
}
100% {
transform: translateY(150%);
}
}
Let’s apply the active classes on click and remove them after the animation has finished running. And that’s it!
const lever = document.querySelector(".lever");
const coffee = document.querySelector(".coffee");
lever.addEventListener("click", function () {
if (lever.classList.contains("lever--active")) {
return;
}
lever.classList.add("lever--active");
coffee.classList.add("coffee--active");
setTimeout(function () {
lever.classList.remove("lever--active");
coffee.classList.remove("coffee--active")
}, 2500);
});
Check out the complete example below, and, as always, feel free to play around with the animations and experiment with other elements, like the speech bubble or making the coffee machine’s lights blink while coffee is pouring out. Have fun!
See the Pen Smashing cat interaction [forked] by Adrian Bece.
ConclusionI hope that this article encourages you to play around and make some wonderful SVG animations and interactions and integrate this workflow into your day-to-day projects. We’ve used only a handful of tricks and CSS properties to create a whole variety of nice effects on the fly. With some extra time, knowledge, and effort, you can create some truly amazing and interactive graphics.
Feel free to reach out on Twitter and share your work. Happy to hear your thoughts and see what you come up with!
References
- SVG explained in 100 seconds, Fireship
- “How SVG Line Animation Works”, Chris Coyier
- SVG specs, W3C
- “
prefers-reduced-motion
: Sometimes less movement is more”, Thomas Steiner