GUISSMO

Making an animated gradient border using CSS

Recently, I had the need to make a button that:

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.

ME PRETENDING TO BE A BORDER

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 360360 degrees, the exact gradient “comes out” of 00 degrees. This 00 and 360360 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 2020 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 (20002000 ms in this case) and dividing by the same number gives a number from 00 to 11 — 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.

Back to Top | Blog RSS Feed