Skip to main content

How to animate SVG images using CSS and JS?

· 4 min read
Lajos Szoke

In this post, I will show you how we implemented the animations where the cat follows the cursor with his eyes when entering username and hides them when entering password.

login

How it works?

We are going to animate SVG using CSS and some JavaScript. You can see it in action here.

Show and hide eyes

The paws movement animation is some kind of a stop motion animation with opacity changes to make the movements smoother. There are separate groups in the SVG for 5 different frames like so:

<g class="hands">
<g class="hands1" data-name="&lt;hands1&gt;">
<path
class="hand1_right"
data-name="&lt;hand1_right&gt;"
d="..."
fill="#c1272d"
></path>
<path
class="hand1_left"
data-name="&lt;hand1_left&gt;"
d="..."
fill="#c1272d"
></path>
</g>
...
<g class="hands5" data-name="&lt;hands5&gt;">
<path
class="hand5_right"
data-name="&lt;hand5_right&gt;"
d="..."
fill="#c1272d"
></path>
<path
class="hand5_left"
data-name="&lt;hand5_left&gt;"
d="..."
fill="#c1272d"
></path>
</g>
</g>

I have added the .pawsCSS class to all groups and each group/frame has an indexed .pawsN class too.

The animation starts by putting .showEyes or .hideEyes CSS classes on our paws group in the SVG. These classes will start the animation by showing/hiding the frames with different paw states with delay:

Our CSS looks something like this:

.paws.showEyes {
opacity: 1;
}

.paws.showEyes .paws5 {
animation: changeFrameShow 0.025s;
}

.paws.showEyes .paws4 {
animation: changeFrameShow 0.025s;
animation-delay: 0.02s;
}

.paws.showEyes .paws3 {
animation: changeFrameShow 0.025s;
animation-delay: 0.045s;
}

.paws.showEyes .paws2 {
animation: changeFrameShow 0.025s;
animation-delay: 0.07s;
}

.paws.showEyes .paws1 {
animation: lastFrameShow 0.025s;
animation-fill-mode: forwards;
animation-delay: 0.095s;
}

We also have the changeFrameShow and lastFrameShow keyframes with some opacity changes for smoother animation:

@@keyframes changeFrameShow {
0% {
opacity: 1;
}
99% {
opacity: 1;
}
100% {
opacity: 0;
}
}

@@keyframes lastFrameShow {
0% {
opacity: 1;
}
99% {
opacity: 1;
}
100% {
opacity: 0;
}
}

Following the caret position in the text input with the animation of the eyes.

How to move the eyes?

On every input value change, we should position the eyes to look at the caret position of the input. Let’s create a function called moveEyes. Attaching it to the input’s change listeners will set a transform on the .eyes group in the SVG.

moveEyes = function (e) {
var input = e.target;
var coordinates = that.getCursorXY(input);
var newLeft = Math.min(
coordinates.x - input.scrollLeft,
input.offsetLeft + input.offsetWidth,
);
newLeft = (newLeft * 10) / input.offsetWidth - 5;
$('.eyes').setAttribute(
'style',
'transform: translate(' + newLeft + 'px, 5px);',
);
};

How to determine the caret position of an input?

This was the hard part of the animation because you can’t directly access the caret position. So I added some dummy elements to the DOM by creating a <div> with the input’s value’s first part (to the selectionEnd) and a <span> with the remaining value from the input (after selectionEnd). The <span>’s offset will give you the caret position which you just have to correct by the input’s original offset:

getCursorXY = function (input) {
const inputX = input.offsetLeft;
const inputY = input.offsetTop;
// create a dummy element that will be a clone of our input
const div = document.createElement('div');
// we need a character that will replace whitespace when filling our dummy element
const swap = '.';
const inputValue = input.value.replace(/ /g, swap);
// set the div content to that of the input up until selection
const textContent = inputValue.substr(0, input.selectionEnd);
// set the text content of the dummy element div
div.textContent = textContent;
div.style.width = 'auto';
// create a marker element to obtain caret position
const span = document.createElement('span');
// give the span the textContent of remaining content so that the recreated dummy element is as close as possible
span.textContent = inputValue.substr(input.selectionEnd) || '.';
// append the span marker to the div
div.appendChild(span);
// append the dummy element to the body
document.body.appendChild(div);
// get the marker position, this is the caret position top and left relative to the input
const spanX = span.offsetLeft;
const spanY = span.offsetTop;
// lastly, remove that dummy element
document.body.removeChild(div);
// return an object with the x and y of the caret. account for input positioning so that you don't need to wrap the input
return {
x: inputX + spanX,
y: inputY + spanY,
};
};