Carolyn Meete
Carolyn Meete

Reputation: 79

How I can animate circular progress bar from left to right?

I have a circle progress bar, only with HTML and CSS, I used keyframes for loading (animation). But the loading is from right to left I wanna reverse it. I edit my CSS keyframes but nothing at all. I try also animation reverse again nothing.

Fiddle: https://jsfiddle.net/d20wu8e4/

My Result (image): https://ibb.co/0KCSsZY

What I want: https://ibb.co/MGCpHqS

* {
 box-sizing:border-box;
}
.progress {
  width: 150px;
  height: 150px;
  background: none;
  margin: 0 auto;
  box-shadow: none;
  position: relative;
}

.progress:after {
  content: "";
  width: 100%;
  height: 100%;
  border-radius: 50%;
  border: 3px solid #fff;
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0.5;
}

.progress>span {
  width: 50%;
  height: 100%;
  overflow: hidden;
  position: absolute;
  top: 0;
  z-index: 1;
}

.progress .progress-left {
  left: 0;
}

.progress .progress-bar {
  width: 100%;
  height: 100%;
  background: none;
  border-width: 2px;
  border-style: solid;
  position: absolute;
  top: 0;
}

.progress .progress-left .progress-bar {
  left: 100%;
  border-top-right-radius: 80px;
  border-bottom-right-radius: 80px;
  border-left: 0;
  -webkit-transform-origin: center left;
  transform-origin: center left;
}

.progress .progress-right {
  right: 0;
}

.progress .progress-right .progress-bar {
  left: -100%;
  border-top-left-radius: 80px;
  border-bottom-left-radius: 80px;
  border-right: 0;
  -webkit-transform-origin: center right;
  transform-origin: center right;
  animation: loading 1.8s linear forwards;
}

.progress .progress-value {
  width: 79%;
  height: 79%;
  border-radius: 50%;
  background: none;
  font-size: 24px;
  color: black;
  line-height: 135px;
  text-align: center;
  position: absolute;
  top: 5%;
  left: 5%;
}

.progress.one .progress-bar {
  border-color: black;
}

.progress.one .progress-left .progress-bar {
  animation: loading-1 1s linear forwards 1.8s;
}

@keyframes loading {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(180deg);
    transform: rotate(180deg);
  }
}

@keyframes loading-1 {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(90deg);
    transform: rotate(90deg);
  }
}
<div class="container bg-danger">
  <div class="row mt-5">
    <div class="progress one">
      <span class="progress-left">
                <span class="progress-bar"></span>
      </span>
      <span class="progress-right ">
                <span class="progress-bar"></span>
      </span>
      <div class="progress-value">73%</div>
    </div>
  </div>
</div>

Upvotes: 3

Views: 2593

Answers (2)

Ori Drori
Ori Drori

Reputation: 191986

This progress works in new blink/webkit browsers since it uses conic-gradient(). In addition, to change the progress we use css variables, so animation will require JS.

The idea is to create a conic gradient of black to transparent, and change the degrees according to the progress. To get a line instead of a circle, I use an inner gradient from white to white, that doesn't cover the border (background-clip: content-box) as suggested by @TemaniAfif.

Play with the values of the input box to see the progress.

const progress = document.querySelector('.circular-progress')

const updateProgress = value => {
  progress.style.setProperty('--percentage', `${value * 3.6}deg`)
  progress.innerText = `${value}%`
}

updateProgress(36)

document.querySelector('input')
  .addEventListener('input', e => {
    updateProgress(e.currentTarget.value)
  })
.circular-progress {
  display: flex;
  width: 150px;
  height: 150px;
  border:5px solid transparent;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  font-size: 1.5em;
  background:
    linear-gradient(#fff, #fff) content-box no-repeat,
    conic-gradient(black var(--percentage,0), transparent var(--percentage,0)) border-box; 
  --percentage: 0deg;
}
<div class="circular-progress"></div>

<br />

Progress value: <input type="number" min="0" max="100" value="36">

And for the other direction (added by @TemaniAfif):

const progress = document.querySelector('.circular-progress')

const updateProgress = value => {
  progress.style.setProperty('--percentage', `${value * 3.6}deg`)
  progress.innerText = `${value}%`
}

updateProgress(36)

document.querySelector('input')
  .addEventListener('input', e => {
    updateProgress(e.currentTarget.value)
  })
.circular-progress {
  display: flex;
  width: 150px;
  height: 150px;
  border:5px solid transparent;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  font-size: 1.5em;
  background:
    linear-gradient(#fff, #fff) content-box no-repeat,
    conic-gradient(from calc(-1*var(--percentage)), black var(--percentage,0), transparent var(--percentage,0)) border-box; 
  --percentage: 0deg;
}
<div class="circular-progress"></div>

<br />

Progress value: <input type="number" min="0" max="100" value="36">

A variation on the same idea, is to create progress circle with multiple colors, and then hide it using a gradient from transparent to white. Make the transparent area bigger to expose the colored line.

const progress = document.querySelector('.circular-progress')

const updateProgress = value => {
  progress.style.setProperty('--percentage', `${value * 3.6}deg`)
  progress.innerText = `${value}%`
}

updateProgress(80)

document.querySelector('input')
  .addEventListener('input', e => {
    updateProgress(e.currentTarget.value)
  })
.circular-progress {
  display: flex;
  width: 150px;
  height: 150px;
  border: 5px solid transparent;
  border-radius: 50%;
  align-items: center;
  justify-content: center;
  font-size: 1.5em;
  background: 
    linear-gradient(#fff, #fff) content-box no-repeat,
    conic-gradient(transparent var(--percentage, 0), white var(--percentage, 0)) border-box,
    conic-gradient(green 120deg, yellow 120deg 240deg, red 240deg) border-box;
  --percentage: 0deg;
}
<div class="circular-progress"></div>

<br /> Progress value: <input type="number" min="0" max="100" value="80">

Upvotes: 5

Temani Afif
Temani Afif

Reputation: 273010

As I commented, the trivial solution is to rotate the whole animation:

* {
 box-sizing:border-box;
}
.progress {
  width: 150px;
  height: 150px;
  background: none;
  margin: 0 auto;
  box-shadow: none;
  position: relative;
  transform: scaleX(-1);
}

.progress-value {
  transform: scaleX(-1);
}

.progress:after {
  content: "";
  width: 100%;
  height: 100%;
  border-radius: 50%;
  border: 3px solid #fff;
  position: absolute;
  top: 0;
  left: 0;
  opacity: 0.5;
}

.progress>span {
  width: 50%;
  height: 100%;
  overflow: hidden;
  position: absolute;
  top: 0;
  z-index: 1;
}

.progress .progress-left {
  left: 0;
}

.progress .progress-bar {
  width: 100%;
  height: 100%;
  background: none;
  border-width: 2px;
  border-style: solid;
  position: absolute;
  top: 0;
}

.progress .progress-left .progress-bar {
  left: 100%;
  border-top-right-radius: 80px;
  border-bottom-right-radius: 80px;
  border-left: 0;
  -webkit-transform-origin: center left;
  transform-origin: center left;
}

.progress .progress-right {
  right: 0;
}

.progress .progress-right .progress-bar {
  left: -100%;
  border-top-left-radius: 80px;
  border-bottom-left-radius: 80px;
  border-right: 0;
  -webkit-transform-origin: center right;
  transform-origin: center right;
  animation: loading 1.8s linear forwards;
}

.progress .progress-value {
  width: 79%;
  height: 79%;
  border-radius: 50%;
  background: none;
  font-size: 24px;
  color: black;
  line-height: 135px;
  text-align: center;
  position: absolute;
  top: 5%;
  left: 5%;
}

.progress.one .progress-bar {
  border-color: black;
}

.progress.one .progress-left .progress-bar {
  animation: loading-1 1s linear forwards 1.8s;
}

@keyframes loading {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(180deg);
    transform: rotate(180deg);
  }
}

@keyframes loading-1 {
  0% {
    -webkit-transform: rotate(0deg);
    transform: rotate(0deg);
  }
  100% {
    -webkit-transform: rotate(90deg);
    transform: rotate(90deg);
  }
}
<div class="progress one">
  <span class="progress-left">
                <span class="progress-bar"></span>
  </span>
  <span class="progress-right ">
                <span class="progress-bar"></span>
  </span>
  <div class="progress-value">73%</div>
</div>

By the way here is another idea that rely on less code. The trick is to consider clip-path where you will adjust the position of the different points in order to create the needed animation

.box {
  width:150px;
  height:150px;
  margin:20px;
  box-sizing:border-box;
  
  font-size:30px;
  display:flex;
  align-items:center;
  justify-content:center;
  position:relative;
  z-index:0;
}
.box:before {
  content:"";
  position:absolute;
  z-index:-1;
  top:0;
  left:0;
  right:0;
  bottom:0;
  border:5px solid #000;
  border-radius:50%;
  transform:rotate(45deg);
  clip-path:polygon(50% 50%,0 0,0 0,0 0, 0 0,0 0);
  animation:change 2s linear forwards;
}

@keyframes change {
  25% {
    clip-path:polygon(50% 50%,0 0,   0 100%,0 100%,0 100%,0 100%);
  }
  50% {
    clip-path:polygon(50% 50%,0 0,0 100%,   100% 100%, 100% 100%,100% 100%);
  }
  75% {
    clip-path:polygon(50% 50%,0 0,0 100%,100% 100%,    100% 0,100% 0);
  }
  100% {
    clip-path:polygon(50% 50%,0 0,0 100%,100% 100%, 100% 0,     0% 0%);
  }
}

body {
 background:pink;
}
<div class="box">
  73%
</div>

To better understand the animation, add background and remove the radius. We basically have 6 points in the polygon where 2 are fixed (the center (50% 50%) and top one (0 0)) then we move the 4 others to put them in the corners. The trick is to move them together and we leave one at each corner (each state of the animation).

.box {
  width:100px;
  height:100px;
  margin:50px;
  box-sizing:border-box;
  
  font-size:30px;
  display:flex;
  align-items:center;
  justify-content:center;
  position:relative;
  z-index:0;
  background:rgba(0,0,0,0.5);
}
.box:before {
  content:"";
  position:absolute;
  z-index:-1;
  top:0;
  left:0;
  right:0;
  bottom:0;
  border:5px solid #000;
  background:red;
  transform:rotate(45deg);
  clip-path:polygon(50% 50%,0 0,0 0,0 0, 0 0,0 0);
  animation:change 5s linear forwards;
}

@keyframes change {
  25% {
    clip-path:polygon(50% 50%,0 0,   0 100%,0 100%,0 100%,0 100%);
  }
  50% {
    clip-path:polygon(50% 50%,0 0,0 100%,   100% 100%, 100% 100%,100% 100%);
  }
  75% {
    clip-path:polygon(50% 50%,0 0,0 100%,100% 100%,    100% 0,100% 0);
  }
  100% {
    clip-path:polygon(50% 50%,0 0,0 100%,100% 100%, 100% 0,     0% 0%);
  }

}

body {
 background:pink;
}
<div class="box">
  73%
</div>

With this code you have the full animation, simply adjust the final state or remove some states to stop it where you want.

Ex with 75% (we remove the last state)

.box {
  width:150px;
  height:150px;
  margin:20px;
  box-sizing:border-box;
  
  font-size:30px;
  display:flex;
  align-items:center;
  justify-content:center;
  position:relative;
  z-index:0;
}
.box:before {
  content:"";
  position:absolute;
  z-index:-1;
  top:0;
  left:0;
  right:0;
  bottom:0;
  border:5px solid #000;
  border-radius:50%;
  transform:rotate(45deg);
  clip-path:polygon(50% 50%,0 0,0 0,0 0, 0 0,0 0);
  animation:change 3s linear forwards;
}

@keyframes change {
  33% {
    clip-path:polygon(50% 50%,0 0,   0 100%,0 100%,0 100%,0 100%);
  }
  66% {
    clip-path:polygon(50% 50%,0 0,0 100%,   100% 100%, 100% 100%,100% 100%);
  }
  100% {
    clip-path:polygon(50% 50%,0 0,0 100%,100% 100%,    100% 0,100% 0);
  }
}
body {
 background:pink;
}
<div class="box">
  75%
</div>

With 66% (we remove the last state and we change the percentage of the third one)

.box {
  width:150px;
  height:150px;
  margin:20px;
  box-sizing:border-box;
  
  font-size:30px;
  display:flex;
  align-items:center;
  justify-content:center;
  position:relative;
}
.box:before {
  content:"";
  position:absolute;
  z-index:-1;
  top:0;
  left:0;
  right:0;
  bottom:0;
  border:5px solid #000;
  border-radius:50%;
  transform:rotate(45deg);
  clip-path:polygon(50% 50%,0 0,0 0,0 0, 0 0,0 0);
  animation:change 2s linear forwards;
}

@keyframes change {
  33% {
    clip-path:polygon(50% 50%,0 0,   0 100%,0 100%,0 100%,0 100%);
  }
  66% {
    clip-path:polygon(50% 50%,0 0,0 100%,   100% 100%, 100% 100%,100% 100%);
  }
  100% {
    clip-path:polygon(50% 50%,0 0,0 100%,100% 100%,    100% 0,100% 40%);
  }
}
<div class="box">
  75%
</div>

with 10% (only one state)

.box {
  width:150px;
  height:150px;
  margin:20px;
  box-sizing:border-box;
  
  font-size:30px;
  display:flex;
  align-items:center;
  justify-content:center;
  position:relative;
}
.box:before {
  content:"";
  position:absolute;
  z-index:-1;
  top:0;
  left:0;
  right:0;
  bottom:0;
  border:5px solid #000;
  border-radius:50%;
  transform:rotate(45deg);
  clip-path:polygon(50% 50%,0 0,0 0,0 0, 0 0,0 0);
  animation:change 1s linear forwards;
}

@keyframes change {
  100% {
    clip-path:polygon(50% 50%,0 0,   0 40%,0 40%,0 40%,0 40%);
  }
}
body {
 background:pink;
}
<div class="box">
  10%
</div>

Upvotes: 4

Related Questions