I was recently reading the Vue documentation and came across a mention of Pinia, a library for state management. And when I clicked on the link, this library won me over thanks to its creative logo. It would seem like this is just a documentation site, where all you need to post is code descriptions and examples. So, it was a surprise to me when Pinia started following my cursor. I even forgot my original goal and simply started searching for all her reactions. Such attention to detail certainly deserves praise. This cheerful character blinks, opens its mouth when hovering and follows the cursor with its eyes - in other words, it is interactive and seems to be alive.
And I wondered - how is it implemented under the hood? And how would I implement the same thing?
Pinia Alchemy
The logo is designed as a separate Vue component, inside which all the magic happens. The component uses the full power of the Vue ecosystem, as can be seen from the imports
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from 'vue'
import { useSpring } from 'vue-use-spring'
import {
useMouse,
useEventListener,
useDebounceFn,
useIdle,
whenever,
} from '@vueuse/core'
This approach works great for Vue, but what if you need to embed an animated SVG file on a React website? Fortunately, SVG can be animated without frameworks. It should be noted right away that SVG animations can be created using various tools such as SVGator, but browser APIs are sufficient for simple animations. In general, I identify four types of SVG animation:
- SMIL animation,
- pure CSS animation,
- attribute driven CSS animation and direct styles manipulation via JS,
- Web Animations API.
We'll look at all of these options using a practical example - I created a character that Pinia might be familiar with.
Meet Mr. Pearman
To animate an SVG, you need to be sure of the correct structure and layer placement. When using ready-made SVGs, you need to be prepared for limitations and adapt to its styles. That's why I prefer to design the character behavior first and then implement it myself. This serious guy was created with Inkscape.
For this character, I plan to animate:
- cape movement (to make it look more epic),
- facial expressions when hovered over (this guy doesn't like being pointed at),
- color change when clicked (his mood gradually worsens after each click, but returns to its original state after the cursor moves out),
- working out with a dumbbell while waiting for the cursor movement (Mr. Pearman has to be strong to do justice).
A good solution is to group elements that should or can be transformed at the same time. I also recommend checking in an SVG editor how the image should look at different points in the animation to make sure you've made the right choice. And the most important thing is not to complicate the SVG.
Once the image is ready, you can start writing the animation in any text editor.
Animating Mr. Pearman
The SMIL animation looks the most organic for SVG. Essentially it all comes down to choosing the right element (animate, animateTransform, animateMotion) and passing the correct attributes. I will use animate to realize the cape emovement:
<path
style="fill:#4b7229;fill-opacity:1;stroke-width:0.264583"
d="m 146.05723,123.12633 c 0,0 41.89228,89.47314 45.40039,99.76848 3.50811,10.29534 -8.11042,8.66023 -14.26613,6.44205 -4.60132,-1.65806 -4.47508,-6.82347 -12.01742,-5.92669 -5.21874,0.6205 -7.61486,8.14833 -12.74747,9.27818 -16.81689,3.70192 -33.45177,-10.04425 -50.67129,-10.05122 -18.861521,-0.008 -36.908207,14.0719 -55.512045,10.96477 -4.649159,-0.77648 -7.517475,-6.97037 -12.229259,-7.09955 -5.107148,-0.14002 -8.571991,7.45123 -13.657138,6.95742 -4.019026,-0.39028 -10.8529692,-3.65626 -9.53423,-7.47278 18.178826,-52.61078 46.809504,-105.37833 46.809504,-105.37833 z"
>
<animate
attributeName="d"
dur="4s"
repeatCount="indefinite"
fill="freeze"
values="m 146.05723,123.12633 c 0,0 41.89228,89.47314 45.40039,99.76848 3.50811,10.29534 -8.11042,8.66023 -14.26613,6.44205 -4.60132,-1.65806 -4.47508,-6.82347 -12.01742,-5.92669 -5.21874,0.6205 -7.61486,8.14833 -12.74747,9.27818 -16.81689,3.70192 -33.45177,-10.04425 -50.67129,-10.05122 -18.861521,-0.008 -36.908207,14.0719 -55.512045,10.96477 -4.649159,-0.77648 -7.517475,-6.97037 -12.229259,-7.09955 -5.107148,-0.14002 -8.571991,7.45123 -13.657138,6.95742 -4.019026,-0.39028 -10.8529692,-3.65626 -9.53423,-7.47278 18.178826,-52.61078 46.809504,-105.37833 46.809504,-105.37833 z;
m 146.05723,123.12633 c 0,0 33.46693,74.61111 35.60848,80.44234 2.14155,5.83123 4.11806,9.17987 2.2934,10.56986 -5.76505,4.39171 -9.62872,-1.92751 -17.17106,-1.03073 -5.21874,0.6205 -5.23418,7.59514 -9.26877,9.27818 -17.15536,7.15639 -37.18589,0.86369 -55.76397,0.25115 -14.805226,-0.48815 -25.545341,1.96964 -44.329665,-3.12698 -4.844665,-1.31447 -14.28973,0.65633 -21.238085,-0.7204 -4.016258,-0.79577 -6.395465,-6.68193 -11.50726,-4.29596 -3.574065,1.66822 -10.079924,2.27042 -8.761185,-1.5461 18.178826,-52.61078 41.713027,-92.33903 41.713027,-92.33903 z;
m 146.05723,123.12633 c 0,0 41.89228,89.47314 45.40039,99.76848 3.50811,10.29534 -8.11042,8.66023 -14.26613,6.44205 -4.60132,-1.65806 -4.47508,-6.82347 -12.01742,-5.92669 -5.21874,0.6205 -7.61486,8.14833 -12.74747,9.27818 -16.81689,3.70192 -33.45177,-10.04425 -50.67129,-10.05122 -18.861521,-0.008 -36.908207,14.0719 -55.512045,10.96477 -4.649159,-0.77648 -7.517475,-6.97037 -12.229259,-7.09955 -5.107148,-0.14002 -8.571991,7.45123 -13.657138,6.95742 -4.019026,-0.39028 -10.8529692,-3.65626 -9.53423,-7.47278 18.178826,-52.61078 46.809504,-105.37833 46.809504,-105.37833 z"
/>
</path>
As you can see SMIL allows to realize SVG path morphing, while CSS solution is still not baseline.
Pure CSS animations are my favorite in most cases, allowing to attach animations and transitions not only to elements but also to their pseudo-states. Here I will use the following CSS to animate :hover pseudostate:
<style>
#mouth {
transition: transform 200ms linear;
transform-origin: center;
}
#right_eye > *, #left_eye > * {
transition: ry 200ms linear;
}
#left_eyebrow {
transition: transform 200ms linear;
transform-origin: center;
}
svg:has(#figure:hover) {
#mouth {
transform: skewY(-10deg);
}
#right_eye > *, #left_eye > * {
ry: 2px;
}
#left_eyebrow {
transform: skewY(10deg);
}
}
</style>
To implement a color change animation, we need to store and compare states - this is where JS comes in handy. In fact, you'll only end up here if you're lacking in CSS or SMIL capabilities. We will simply set a new attribute value based on its current value, and at the end of the timeout, we will return the initial color:
<style>
svg {
transition: filter 250ms linear;
filter: grayscale(0%);
}
#figure {
cursor: pointer;
}
svg[data-lvl="1"] {
filter: grayscale(30%) contrast(150%);
}
svg[data-lvl="2"] {
filter: grayscale(70%) contrast(200%);
}
svg[data-lvl="3"] {
filter: grayscale(100%) contrast(200%);
}
</style>
<script type="text/javascript">
//<![CDATA[
const svg = document.currentScript.parentElement;
const figure = svg.querySelector('#figure');
figure.addEventListener('click', () => {
const level = +svg.dataset.lvl || 0;
if (level < 3) {
const nextLevel = level + 1;
svg.dataset.lvl = nextLevel;
}
});
figure.addEventListener('mouseleave', () => {
delete svg.dataset.lvl;
});
//]]>
</script>
Sometimes it becomes necessary to dynamically identify keyframes and accurately monitor the progress of animation - this is where the Web Animations API comes to the rescue. Before using this tool, I always ask myself the question - do I really need it? Sometimes the reason for the complexity of the code is poor documentation of requirements, so it's better to clarify all the conditions and understand the scenario before starting to implement it. CSS and attribute bindings may be enough for you. Let's use the API to create training animations that depend on whether the cursor is over Pearman:
<style>
#left_hand_bottom {
transform-origin: 25.639345px 180.89267px;
}
</style>
<script
type="text/javascript"
id="script3"
>
//<![CDATA[
const doc = document.documentElement;
const svgElem = document.currentScript.parentElement;
const hand = svgElem.querySelector('#left_hand_bottom');
const figureElem = svgElem.querySelector('#figure');
let timeoutId;
let animation;
let reset;
function start() {
if (hand.getAnimations().length) return;
else if(figureElem.matches('*:hover')) {
animation = hand.animate([
{
transform: 'rotate(0deg)'
},
{
transform: 'rotate(-65deg)'
},
{
transform: 'rotate(0deg)'
}
], {
duration: 8000, easing: 'cubic-bezier(.2,.34,0,1.05)', iterations: 'Infinity'
});
} else {
animation = hand.animate([
{
transform: 'rotate(0deg)'
},
{
transform: 'rotate(-45deg)'
},
{
transform: 'rotate(0deg)'
}
], {
duration: 2000, easing: 'ease', iterations: 'Infinity'
});
}
}
function stop() {
if (animation && !reset) {
animation.pause();
reset = hand.animate([
{
transform: 'rotate(0deg)'
}
], {
duration: 500, easing: 'ease'
});
reset.finished.then(() => {
animation.cancel();
animation = undefined;
reset.cancel();
reset = undefined;
});
}
}
doc.addEventListener('mousemove', () => {
clearTimeout(timeoutId);
stop();
timeoutId = setTimeout(() => start(), 5000);
});
//]]>
</script>
The Web Animation API is so powerful that it cannot be described in one example. But with great power comes great responsibility, and the likelihood of mistakes also increases. Therefore, I rather consider it as a foundation for writing specialized libraries than as a direct animation tool.
So, we can collect all the animations in one SVG to simply use in a browser. As a result, we get the following SVG
As you can see, SVG is embedded directly into the HTML document so that its scripts can reach the document. There is also the option of embedding interactive SVGs via iframe, but then they will be in their own HTML document. This is one of the nuances to keep in mind - a regular img tag will not show all the embedded animations. This is why Pinia can catch cursor movements - it is built into the Vue component.
Conclusion
In fact, I'm really inspired by the fact that we can animate almost anything in the browser without any external dependencies. And interactive SVGs can not only attract attention but also make a website more lively and interesting. The key with animation is not to overdo it.
Like any front-end component, animation should serve a purpose, not just distract from the task at hand. And if you focus on small features, there will usually be enough CSS animations to make the site pleasant to interact with.
In addition, you need to be prepared to embed SVG containing scripts directly into HTML or via an iframe. This is not always acceptable, and extra scripts on the page can cause chaos.
I hope that the world of SVG animations has become closer to you after reading.
Enjoy your Frontend Development!

Top comments (1)
Wow! What a fun character and animation! 🍐