Using javascript to animate an svg is recommended for more complex, dynamic animations. Javascript allows you to animate a wider range of svg attributes and elements, like the viewbox and filters, making it much more powerful option than CSS animations.
Animation is an illusion created by showing a series of slightly different still images in rapid succession. If the images change slowly we won't perceive the images as animation. We'll just perceive a series of still pictures. The faster the images change, the smoother and more fluid the motion appears.
The rate at which the images change is called the frame rate. The faster the frame rate, the smoother the animation.
Browsers optimally render at a frame rate of about 60 fps, but the framerate can be affected by lots of factors.
One way to using javascript is to dynamically add and/or remove css transition or animation classes.
For example, let's add a rotation animation to this square
1
2
3
4
5
<svg width="500" height="500" viewbox="0 0 500 500">
<g transform="translate(250 250)">
<rect class="square" x="-150" y="-150" width="300" height="300" fill="cornflowerblue" rx="10" ry="10" />
</g>
</svg>
First, let's set up a style that sets up the initial rotation transform and the transition properties (square). We'll create a second class, (spin-style-transition), that we'll apply to the element dynamically using javascript.
1
2
3
4
5
6
7
.square {
transform: rotate(0deg);
transition: 500ms ease-in-out;
}
.spin-style-transition {
transform: rotate(360deg);
}
We'll use querySelector to get a reference to the square using it's id:
1
const square = document.querySelector(".square");
Next we'll add a click event listener to the square.
1
2
3
4
const square = document.querySelector(".square");
square.addEventListener("click", () => {
// code goes here
})
Finally we'll use the classList property that returns a DOMTokenList of classes currently applied to an element. The DomTokenList has a contains method that we can use to see if the spin-style-transition class is part of the classlist. If so, we'll remove it. If not, we'll add it.
1
2
3
4
5
6
7
const square = document.querySelector(".square");
square.addEventListener("click", () => {
const list = square.classList;
list.contains("spin-style-transition")
? square.classList.remove("spin-style-transition")
: square.classList.add("spin-style-transition");
});
We can also toggle keyframe animations.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.square {
transform: rotate(0deg);
}
.spin-style-animation {
animation: spin 1s ease-in-out;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
1
2
3
4
5
6
const square = document.querySelector("#square");
square.addEventListener("click", () => {
const list = square.classList;
list.contains("spin-style-animation")
? square.classList.remove("spin-style-animation")
: square.classList.add("spin-style-animation")})
If we simply try to swap out the transitions for the keyframe animation, the animation doesn't toggle like it did with the transitions. The first click works, but nothing seems to happen with the second click. Why?
Removing the animation class doesn't trigger a transition - the element just reverts back to what it was before. On the second click, the animation class is removed, and it reverts to the original transform rotation, which is 0. But it doesn't transition to the original value, it just "pops" back into place.
The solution: we swap animation classes. Let's create the forwards (spin) and backwards (spinBack) animations:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.square {
transform: rotate(0deg);
}
.spin-style-animation {
animation: spin 1s ease-in-out;
}
.spin-back-style-animation {
animation: spinBack 1s ease-in-out;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes spinBack {
from {
transform: rotate(360deg);
}
to {
ransform: rotate(0deg);
}
}
Now we'll use javascript to swap them out.
1
2
3
4
5
6
7
8
9
10
square.addEventListener("click", () => {
const list = square.classList;
if(list.contains("spin-style-animation")){
square.classList.remove("spin-style-animation")
square.classList.add("spin-back-style-animation")
} else {
square.classList.add("spin-style-animation")
square.classList.remove("spin-back-style-animation")
}
})
Now it works!
Another tool we can use to make css animations more robust is the "animationend" event. It allows us to listen for the end of css animations using javascript.
In the following example we'll listen for the end of one animation and trigger the next when it occurs. Here's the animation CSS classes and keyframes. TLDR; the shrinkAnimation shrinks the cube and the expandAnimation expands it.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
.shrinkAnimation {
animation-name: shrink;
animation-duration: 1s;
animation-timing-function: ease-in-out;
animation-fill-mode: forwards;
}
@keyframes shrink {
from {
transform: scale(1);
fill: conflowerblue;
}
to {
transform: scale(0.5);
fill: red;
}
}
.expandAnimation {
animation-name: expand;
animation-duration: 1s;
animation-timing-function: ease-in-out;
animation-fill-mode: both;
}
@keyframes expand {
from {
transform: scale(0.5);
fill: red;
}
to {
transform: scale(1);
fill: cornflowerblue;
}
}
First we'll add a click event handler to the square, and use it to add the shrink animation.
1
2
3
4
5
const square = document.querySelector("#square");
square.addEventListener("click", () => {
square.classList.add("shrinkAnimation")
})
Next we'll add an "animationend" event listener to the square, and use it to both remove the shrinkAnimation class and add the expandAnimation:
1
2
3
4
5
6
7
8
9
10
const square = document.querySelector("#square");
square.addEventListener("click", () => {
square.classList.add("shrinkAnimation")
})
square.addEventListener('animationend', () => {
square.classList.remove("shrinkAnimation")
square.classList.add("expandAnimation")
});
You may have noticed that you have to click the "reset" button in order to run through the animation again. Why can't we just click the square to have the animation repeat? Well, after the animation shrinks and expands the first time the square still has the "expandAnimation" class as part of it's classList. When you click the square the second time, you add the shrinkAnimation class, but it's cancelled out by the expandAnimation class. In order to repeat the shrinkAnimation, we'll need to remove the expandAnimation class first:
1
2
3
4
5
6
7
8
9
10
11
12
13
const square = document.querySelector("#square");
square.addEventListener("click", () => {
if(square.classList.contains("expandAnimation")){
square.classList.remove("expandAnimation");
}
square.classList.add("shrinkAnimation")
})
square.addEventListener('animationend', () => {
square.classList.remove("shrinkAnimation")
square.classList.add("expandAnimation")
});