htmlcsscss-animationscss-gradients

CSS animated gradient border on a DIV


I'm trying to create a loading DIV that has a border that looks like an indeterminate progress ring spinner.

I'm pretty close based on one of the examples on https://css-tricks.com/gradient-borders-in-css/

This is great when the border doesn't rotate. When you set the border in the :before element to match the transparent border in the gradient-box element then the static gradient border looks perfect.

However, once the animation is added, because the whole :before element rotates you get a pretty odd effect - as shown in the example below.

.gradient-box {
  
  display: flex;
  align-items: center;
  width: 90%;
  margin: auto;
  max-width: 22em;

  position: relative;
  padding: 30% 2em;
  box-sizing: border-box;

  border: 5px solid blue;
  color: #FFF;
  background: #000;
  background-clip: padding-box; /* !importanté */
  border: solid 5px transparent; /* !importanté */
  border-radius: 1em;

}

.gradient-box:before {
    content: '';
    position: absolute;
    top: 0; right: 0; bottom: 0; left: 0;
    z-index: -1;
    margin: -35px; /* !importanté */
    border-radius: inherit; /* !importanté */
    background: conic-gradient(#0000ff00, #ff0000ff);
    -webkit-animation: rotate-border 5s linear infinite;
    -moz-animation: rotate-border 5s linear infinite;
    -o-animation: rotate-border 5s linear infinite;
    animation: rotate-border 3s linear infinite;
}

@keyframes rotate-border {
    to {
        transform: rotate(360deg);
    }
}

html { height: 100%; background: #000; display: flex; }
body { margin: auto; }
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Loading DIV Test</title>
</head>
<body>

<div id="loadingBox" class="gradient-box">
<p>Loading.</p>
</div>

</body>

I've tried playing about with overflow: hidden; but the border just disappears.. is there any way to 'mask' the :before element in a way that whatever is behind this loading Div is still visible behind it and so that the border stays as its intended width?

Basically, my goal is that the colour gradient in the border rotates to give the effect of a spinning/rotating edge.


Solution

  • I like your original idea with using overflow: hidden, but to make it work I had to include an extra wrapper div.

    .loading-box-container {
      --size: 200px;
      --radius: 10px;
      position: relative;
      width: var(--size);
      height: var(--size);
      padding: var(--radius);
      border-radius: var(--radius);
      overflow: hidden;
    }
    
    .loading-box {
      position: relative;
      width: 100%;
      height: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      background: #000;
      border-radius: var(--radius);
    }
    
    .loading-box-container::before {
      content: '';
      width: 150%; /* The upscaling allows the box to fill its container even when rotated */
      height: 150%;
      position: absolute;
      top: -25%; left: -25%;
      background: conic-gradient(#0000ff00, #ff0000ff);
      animation: rotate-border 5s linear infinite;
    }
    
    @keyframes rotate-border {
        to {
            transform: rotate(360deg);
        }
    }
    <div class="loading-box-container">
      <div class="loading-box">
        <p>Loading</p>
      </div>
    </div>

    An alternative: Using @property

    There's a much more elegant solution using @property, but unfortunately it only works on Chrome. I'm including here in case one day it becomes more universally supported or support for other browsers isn't important for your use case.

    The conic-gradient function has a parameter that allows you to specify at what angle the gradient starts. If we can animate just that parameter, perhaps using a CSS variable, then we can animate the border with just a single div and without actually rotating anything.

    Unfortunately, without some hinting the browser doesn't know how to transition a CSS variable. Therefore, we use @property to indicate the variable is an angle, telling the browser how to transition it.

    @property --rotation {
      syntax: '<angle>';
      initial-value: 0deg;
      inherits: false;
    }
    
    .loading-box {
      --size: 200px;
      --radius: 10px;
      position: relative;
      width: var(--size);
      height: var(--size);
      display: flex;
      align-items: center;
      justify-content: center;
      color: #fff;
      background: #000;
      border-radius: var(--radius);
      margin: var(--radius);
    }
    
    .loading-box::before {
      --rotation: 0deg;
      content: '';
      width: calc(100% + 2 * var(--radius));
      height: calc(100% + 2 * var(--radius));
      border-radius: var(--radius);
      position: absolute;
      top: calc(-1 * var(--radius)); left: calc(-1 * var(--radius));
      background: conic-gradient(from var(--rotation), #0000ff00, #ff0000ff);
      animation: rotate-border 5s linear infinite;
      z-index: -1;
    }
    
    @keyframes rotate-border {
        to {
            --rotation: 360deg;
        }
    }
    <div class="loading-box">
      <p>Loading</p>
    </div>

    CanIUse for @property indicates this will only work in Chrome and Edge as of this post.