Let’s take a look at how to combine the border-image
property in CSS with animated SVGs that move around a border. In the process, we’ll cover how to hand-craft resizable, nine-slice animated SVGs that you can use not only re-create the effect, but to make it your own.
Here’s what we’re making:
This is actually part of The Skull, a capture-the-flag riddle I’m working on that’s designed to explore the internals of Arduino and its microcontroller. I searched how to animate a border like this, but couldn’t find any useful examples. Most of the stuff I found was about marching ants, but unfortunately, the stroke-dasharray
trick doesn’t work with skulls, let alone more complex shapes.
So, in the spirit of learning and sharing, I’m blogging about it here with you!
background
or border-image
?
Should we use At first, I didn’t even know border-image
was a thing. I tried using a ::before
pseudo-element in my first attempt, and animated its background-position
property. That got me this far:
As you can see, it worked, but completing the border would require at least eight different elements (or pseudo-elements). Not ideal to clutter the HTML like that.
I posted a question on the Israeli CSS developers Facebook group, and everyone pointed me at the border-image
property. It does exactly what it says on the tin: use an image (or CSS gradient) for an element’s border.
To work with border-image
, you have to provide it an image which is used in a nine-slice way (think of a tic-tac-toe board over the image). Each of those nine regions represents a different part of the border: the top, right, left, and bottom, each of the four corners, and then the middle (which is ignored).
For instance, if we just wanted static skulls, we could take advantage of SVG patterns to repeat the skull nine times. First, we define an 24×24 pattern using the skull’s path, and then use this pattern as the fill
for a 72×72 rect:
<svg version="1.1" height="72" width="72" xmlns="http://www.w3.org/2000/svg">
<defs>
<pattern id="skull-fill" width="24" height="24"
patternUnits="userSpaceOnUse">
<path d="..." fill="red"/>
</pattern>
</defs>
<rect fill="url(#skull-fill)" width="72" height="72" />
</svg>
Next, we define a border and set the border-image
on the target element:
.skulls {
border: 24px solid transparent;
border-image: url("https://skullctf.com/images/skull-9.svg") 24 round;
}
And we get a border made out of skulls:
Adding SVG animations
Now we can animate those skulls! It works, uh, for the most part.
The idea is to create a different animation for each region in the border image. For instance, in the top-left corner, we have one skull going from the right-to-left, while a second skull goes from top- to-bottom at the same time.
We’ll animate the transform
property for the movement. We’ll also take advantage of SVG’s <use>
to avoid repeating the lengthy <path>
definition for each skull:
<svg version="1.1" height="96" width="96" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<style>
@keyframes left {to {transform: translate(-32px, 0)}}
@keyframes down {to {transform: translate(0, 32px)}}
</style>
<defs>
<path id="skull" d="..." fill="red"/>
</defs>
<!-- Top-left corner: one skull goes left, another goes down -->
<use href="#skull" x="0" y="0" style="animation: down .4s infinite linear"/>
<use href="#skull" x="32" y="0" style="animation: left .4s infinite linear"/>
</svg>
The SVG animation syntax might look familiar there, because rather than some SVG-specific syntax, like SMIL, it’s just using CSS animations. Cool, right?
This is what we get:
And if we add a grid, we can see how this animation also covers some of the top and left edges as well:
It starts looking more impressive after we add the remaining three edges, thus completely covering all the eight regions of the border image:
<svg version="1.1" height="96" width="96" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<style>
@keyframes left {to {transform: translate(-32px, 0)}}
@keyframes down {to {transform: translate(0, 32px)}}
@keyframes right {to {transform: translate(32px, 0)}}
@keyframes up {to {transform: translate(0, -32px)}}
</style>
<defs>
<path id="skull" d="..." fill="red"/>
</defs>
<!-- Top-left corner: one skull goes left, another goes down -->
<use href="#skull" x="0" y="0" style="animation: down .4s infinite linear"/>
<use href="#skull" x="32" y="0" style="animation: left .4s infinite linear"/>
<!-- Top-right corner: one skull goes up, another goes left -->
<use href="#skull" x="64" y="0" style="animation: left .4s infinite linear"/>
<use href="#skull" x="64" y="32" style="animation: up .4s infinite linear"/>
<!-- Bottom-left corner: one skull goes down, another goes right -->
<use href="#skull" x="0" y="32" style="animation: down .4s infinite linear"/>
<use href="#skull" x="0" y="64" style="animation: right .4s infinite linear"/>
<!-- Bottom-right corner: one skull goes right, another goes up -->
<use href="#skull" x="32" y="64" style="animation: right .4s infinite linear"/>
<use href="#skull" x="64" y="64" style="animation: up .4s infinite linear"/>
</svg>
And this gives us a complete circuit:
Putting everything together, we use the animated SVG we’ve just created as the border-image
, and get the desired result:
I could play with this all day…
Once I got this to work, I started tinkering with the animation properties. This is one of the advantages of using SVGs instead of GIFs: changing the nature of the animation is as easy as changing one CSS property in the SVG source file, and you get to see the result instantly, not to mention the smaller file sizes (especially if you are dealing with gradients), full color support and crisp scaling.
For starters, I tried to see what it would like like if I changed the animation timing function to ease
:
We can also make the skulls fade between red and green:
We can even make the skulls change orientation as they go around the high score table:
Head to the JavaScript tab where you can tinker with the SVG source and try it for yourself.
The giant 🐘 in the room (cough, Firefox)
I was very happy when I first got this to work. However, there are some caveats that you should be aware of. First and foremost, for some reason, Firefox doesn’t render the animation at the edges of the border, only at the corners:
Funny enough, if I changed the SVG to a GIF with the same animation, it worked perfectly fine. But then the edges stop animating on Chrome! 🤦♂️
In any case, it seems to be a browser bug, because if we change the border-image-repeat
property to stretch
, Firefox does animate the edges, but the result is a bit quirky (though it can probably fit the theme of the page):
Changing the border-image-repeat
value to space
also seems to work, but only if the width of the element is not a whole multiple of the skull size, which means we get some gaps in the animation.
I also spotted a few visual issues in cases when the container size is not a multiple of the patch size (32px in this case), such as tiny black lines on the skulls. I suspect this has to do with some floating point rounding issue. It also tends to break when zooming in.
Not perfect, but definitely done! If you want to see the final version in action, you are invited to check out The Skull’s High Scores page. Hopefully, it’ll have some of your names on it very soon!
I wish that my clients would have budget for this :) Greetings from PL.
Thanks! I wish too :)
Insane, I will Animate my whole Background.
Awesome! Please show me the result :)
Nice trick and well written, thank you!
Could you please elaborate on the “dirty trick to load the above (inline) SVG”? Did you use it because URL fragments don’t work with some CSS properties? I ask this because this morning I tried defining
cursor
with a URL fragment pointing to an animated svg (inlined in the document) but it was not rendered. My main motivation was to check if browsers render animation defined in an external SVG.I have tried implementing your animation using
marker-mid
but not with much success andborder-image
is clearly the correct method anyway.Guillaume!
Exactly, as explained here. Did you figure it out or do you still need some help with that?
Hi Uri,
Sorry, I can’t see how the linked section is related to my question.
I was hoping you could explain why you are using
Blob
+createObjectURL
to assign the value toborder-image
in the two CodePens of this section.I already used this “hack”, ans I couldn’t remember why I needed it. It seems that it is not possible to assign certain CSS image properties to an URL pointing to a fragment in the page, eg.
url(#fragment)
, andelement(#fragment)
is currently supported only by Firefox. I think this is the answer that I was waiting for.Sorry, I probably didn’t understand your question correctly! If I recall correctly, I tried
url(#fragment)
at some point too, but it didn’t work. It seemed like the only sensible way would be to use a Blob URL, since neither fragment nor data URL did the trick for me.I didn’t know about
element(#fragment)
. Nice to learn something new!Nice! I remember doing some shenanigans with animated SVG borders on my DA-page, about 10 years ago.
Actually just checked and it’s still working in current mobile Firefox and Chrome
https://deviantart.com/fli-c/journal/pfft-SVG-journal-skin-367755371
Good afternoon, I use much simpler animations in SVG for border-image. I have a product card drawn in a complicated way, and there are little blue flashing lines in one or two places. So when I animate them I have lots of lags on my computer, phone and so on like border image is eating up the performance. Can you help me with this?
Hi there,
I’m trying to replicate your custom border trick, but I get stuck at the first SVG animation level—I don’t really get it. How do you direct the SVG to your own SVG upload icon? Then how do you output it as a GIF? Any help would be welcome!
Thanks