GUISSMO
Making an animated gradient border using CSS
Recently, I had the need to make a button that:
- had rounded borders,
- had text inside,
- had borders which was an animated gradient
So basically this:
And this is possible using the following HTML code and a dash of CSS magic.
<div class="gradient-border">
<button class="button">PRESS ME!</button>
</div>
Setting Up
After some online searches, I didn’t find out how to have borders in CSS with gradients. And so, for my workaround, we will need to layer two elements on top of each other, one slightly smaller, to emulate a bordered element.
To achieve that, we have the following CSS code:
.outside {
/* relevant stuff*/
position: relative;
}
.inside {
/* relevant stuff*/
position: absolute;
top: 5px;
left: 5px;
width: calc(100% - 10px);
height: calc(100% - 10px);
}
The inner element (green) needs to be positioned relative to the outer element (cyan). To do this, we set absolute
as the position
value for the inner element, and relative
for the outer element.
The position: relative
for the outer element is important as we have previously discovered. If we don’t do that, the inner element would be positioned relative to the closest positioned ancestor, which can be anything!
For this example, we want there to be borders on all directions. Therefore, our inner element needs to be “centered”. To do that, simply displace it by the desired with of your border using the top
and left
properties, and then use the calculate function to compute the width and height values which will “center” this element.
Animating Conic Gradients
But of course, we are not here for solid borders. We want gradients. After a quick search, we get the following code:
.gradient {
--angle: 1turn;
border-radius: 15px;
background: black conic-gradient(
rgb(255, 255, 255, 0) 0rad,
rgb(255, 255, 255, 0.6) 1rad,
rgb(255, 255, 255, 0) 3rad
);
}
And hence we will have something like this:
To animate it, however, we use keyframes
and properties
as shown in this page:
@keyframes rotate {
from {
--angle: 0turn;
}
to {
--angle: 1turn;
}
}
@property --angle {
syntax: '<angle>';
initial-value: 0turn;
inherits: false;
}
and of course we adapt our gradient class accordingly
.gradient {
--angle: 1turn;
animation: 2s rotate linear infinite;
border-radius: 15px;
background: black conic-gradient(
rgb(255, 255, 255, 0) calc(0rad + var(--angle)),
rgb(255, 255, 255, 0.6) calc(1rad + var(--angle)),
rgb(255, 255, 255, 0) calc(3rad + var(--angle))
);
}
to get this:
As you can see, it has an ugly discontinuity. So we still have to fix it.
Fixing the Discontinuity
We actually discovered a limitation of conic-gradient. Everything beyond 360 degrees (i.e. 1turn
in our code) is ignored. The conic gradient doesn’t wrap around nicely as if we were in a trigonometry course.
I fixed this by simulating a “second” conic gradient behind the first one, which is just one turn late. This way, as soon as one part of the gradient goes beyond degrees, the exact gradient “comes out” of degrees. This and degree angle is the point of discontinuity we saw earlier.
.gradient-border {
--angle: 1turn;
animation: 2s rotate linear infinite;
border-radius: 15px;
background: black conic-gradient(
rgb(255, 255, 255, 0) calc(-1turn + 0rad + var(--angle)),
rgb(255, 255, 255, 0.6) calc(-1turn + 1rad + var(--angle)),
rgb(255, 255, 255, 0) calc(-1turn + 3rad + var(--angle)),
rgb(255, 255, 255, 0) calc(0rad + var(--angle)),
rgb(255, 255, 255, 0.6) calc(1rad + var(--angle)),
rgb(255, 255, 255, 0) calc(3rad + var(--angle))
);
}
And here is what it now looks like:
Bringing back our “inner layer”, we get this:
Javascript Fallback
If you’re using Safari or some other lame browser, the animation might not have been working this whole time. This is because not all browsers implement this properly. I can’t understand why because everything seems to be supported according to caniuseit, so I must be missing something.
In any case, we don’t have a pure CSS solution yet as of time of writing. So we add the following:
To work around that we add a setInterval
script.
const startTime = Date.now();
setInterval(() => {
const DURATION = 2000;
const NEW_VALUE = `${((Date.now() - startTime) % DURATION) / DURATION}turn`;
const COMPONENT = Array.from(document.getElementsByClassName("gradient-border")).forEach( (component) => {
component.style.setProperty('--angle', NEW_VALUE);
})
}, 20)
Every milliseconds, it changes the value of --angle
depending on the difference between the startTime
(i.e. when the script was loaded) and Date.now()
which gives the current “date” (the number of milliseconds after 1 January 1970). Taking this modulo the intended duration ( ms in this case) and dividing by the same number gives a number from to — which represents the progress of the animation at any given time.
Doing this is needlessly complicated and perhaps at some point in the future, everything will be supported.
Conclusion
So to conclude, if you have a lame browser, this probably wont work:
But adding the Javascript polyfill, this one should:
It’s a great puzzle that I enjoyed doing.
One open problem would be to use math to change the speed of the animation, so you would feel like the “gradient” snake doesn’t speed up on the edges.
Feel free to get in touch if you have any questions, or comments. Or if you have anything to add! That would greatly be appreciated.