Fat Monk
Fat Monk

Reputation: 2265

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.

Upvotes: 0

Views: 4339

Answers (2)

Auroratide
Auroratide

Reputation: 2577

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

  • The outer wrapper defines a padding which serves as the display area for the gradient border
  • The inner div is just the content box with a black background

.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.

Upvotes: 2

John
John

Reputation: 5337

Hi is this what you are looking for?

What I did was I added a new div which will be the "mask" as well as a container div for both the mask and the loadingBox.

I then sized the mask to be a little larger than your visible area, make it a transparent background, and then gave it a large outline the same color as your background to effectively mask out a border. I then fiddled with z-indexs of the mask, the loadingbox and the before. I also added some actual borders on mask to box it out into a nice shape.

Take a look:

.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: -3;
    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; }

.mask {
  position: absolute;
   box-sizing: border-box;
background-color: transparent; 
 outline: 65px solid black;
height: 100%;
  width: 100%;
  border: 2px solid black;
  border-left: 7px solid black;
  border-right: 7px solid black;
  z-index: -1;
}

.container {
  position: relative;
}
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>Loading DIV Test</title>
</head>
<body>
<div class="container">
<div class="mask"></div>
<div id="loadingBox" class="gradient-box">
<p>Loading.</p>
</div>
  </div>
</body>

Upvotes: 0

Related Questions