Search code examples
htmlcssflexboxprogress-barcss-calc

Sophisticated proportional triple element progress bar design using only CSS


I have made a custom progress bar, consisting of three separete parts (a uniquely customisable center piece, a left part and a right part) but I'm having difficulty aligning the center block correctly in all phases.

First I will show the desired end state using three graphical layouts, then I will describe the current problem and finally I will provide my current workaround hack, which is faulty and needs a fix of some sort.


Three Desired States:
Desired outcome of a starting state showing 1% left aligned:
enter image description here

Desired outcome of halfway sate with center block perfectly in the middle at 50%:
enter image description here

Desired end sate with center block perfectly stopping at 100% right aligned:
enter image description here


body{margin: 100px; background: #CCC}

.fullbar{
    background-color: blue;
    width: 100%;
}

.progress{
    background: green;
  margin: 10px 0 0 0;
    text-align: right;
    padding: 0 0px 1px 0px;
    line-height: 5px;
}

.number{
  background: inherit;
  color: #FFF;
    padding: 4px;
    padding: 0 2px 1px 3px;
 
}
<div class="fullbar">
<div class="progress" style="width:50%">
    <div class="number">50%</div>
</div>
</div>


The Problem
The center block should be aligned horizontally perfectly in the middle when the state is 50%. However it is not. The end of the line gets centered, instead of the div containing the actual number "50%".

enter image description here


PS. For an unknown reason, the body of the center block is not correctly rendered in the code view. Perhaps my extensive css resets made my progress bar look differently than the bare code here. But its about the div with class name number that needs to be centered correctly, which is not at the moment.


My Hacky Solution, Not Working Correctly and Not Elegant
I tried wrapping the center piece with width:112% as a hack to the progress bar, to get the center block to perfectly middle, like so:

<div class="fullbar">
  <div style="width:112%">
    <div class="progress" style="width:50%">
      <div class="number">50%</div>
    </div>
  </div>
</div>

However, while this does make the 50% appear perfectly horitontally centered, the end state 100% is now pushed beyond the div boundaries to the right, making the solution incorrect and unusable.


Main Question and First Bounty (50 points)
It would be nice to find another CSS (flex or calc) solution in which all three desirable states (see above three pictures) align perfectly, where the states fit the beginning state, the end state, and everything in between "proportionally".


Bonus Question and Second Bounty (100 points)
Part A) An elegant way to animate the progress (center piece and left colored bar) with only CSS, with an ease-in-ease-out motion, with a delay of 1 second after page load.

Part B) The animation (and number) should start at 0% and the displayed number in the center piece then grows during the animation up to XX% (whatever was set in html as %) and ends with the right progress number and right horizontal progress location.


Solution

  • You can do like below. I am using different colorations to better see the result

    body {
      margin: 100px;
      background: #CCC
    }
    
    .fullbar {
      background-color: blue;
    }
    
    .progress {
      background: lightgreen;
      margin: 10px 0 0 0;
      height: 5px;
      position:relative; /* relative here */
      width:var(--p);
    }
    
    .number {
      position:absolute; /* absolute here */
      background: rgba(255,0,0,0.5);
      left:100%; /* push to the right side */
      transform:translateX(calc(-1*var(--p))); /* offset to the left based on --p */
      top:-10px;
      bottom:-10px;
      color: #FFF;
      padding: 0 2px 1px 3px;
    }
    <div class="fullbar">
      <div class="progress" style="--p:0%">
        <div class="number">0%</div>
      </div>
    </div>
    
    <div class="fullbar">
      <div class="progress" style="--p:20%">
        <div class="number">20%</div>
      </div>
    </div>
    
    <div class="fullbar">
      <div class="progress" style="--p:50%">
        <div class="number">50%</div>
      </div>
    </div>
    
    <div class="fullbar">
      <div class="progress" style="--p:80%">
        <div class="number">80%</div>
      </div>
    </div>
    
    <div class="fullbar">
      <div class="progress" style="--p:100%">
        <div class="number">100%</div>
      </div>
    </div>

    Another idea using only one div:

    body {
      margin: 100px;
      background: #CCC
    }
    
    .progress {
      margin: 20px 0;
      height: 10px;
      position: relative;
      background: linear-gradient(lightgreen 0 0) 0/var(--p) 100% no-repeat blue;
    }
    
    .progress::before {
      content: attr(style);
      font-family: monospace;
      font-size:20px;
      white-space:nowrap;
      text-indent: -4ch;
      overflow: hidden;
      position: absolute;
      background: rgba(255, 0, 0, 0.8);
      border:5px solid transparent;
      top:50%;
      left: var(--p);
      transform: translate(calc(-1*var(--p)),-50%);
      color: #FFF;
    }
    <div class="progress" style="--p:0%"></div>
    
    <div class="progress" style="--p:20%"></div>
    
    <div class="progress" style="--p:50%"></div>
    
    
    <div class="progress" style="--p:80%"></div>
    
    <div class="progress" style="--p:100%"></div>

    Update

    With animation:

    body {
      margin: 100px;
      background: #CCC
    }
    
    .progress {
      margin: 20px 0;
      height: 10px;
      position: relative;
      background: linear-gradient(lightgreen 0 0) 0/var(--p) 100% no-repeat blue;
      animation:p1 1s 1s both;
    }
    
    .progress::before {
      content: attr(style);
      font-family: monospace;
      font-size:20px;
      white-space:nowrap;
      text-indent: -4ch;
      overflow: hidden;
      position: absolute;
      background: rgba(255, 0, 0, 0.8);
      border:5px solid transparent;
      top:50%;
      left: var(--p);
      transform: translate(calc(-1*var(--p)),-50%);
      color: #FFF;
      animation:p2 1s 1s both;
    }
    @keyframes p1 {from {background-size:0 100%}}
    @keyframes p2 {from {left:0;transform: translate(0%,-50%)}}
    <div class="progress" style="--p:0%"></div>
    
    <div class="progress" style="--p:20%"></div>
    
    <div class="progress" style="--p:50%"></div>
    
    <div class="progress" style="--p:80%"></div>
    
    <div class="progress" style="--p:100%"></div>

    For the number animation I would use @property but it's only available on chrome an edge for now:

    body {
      margin: 100px;
      background: #CCC
    }
    
    @property --p {
      syntax: '<number>';
      inherits: true;
      initial-value: 0;
    }
    @property --s {
      syntax: '<integer>';
      inherits: true;
      initial-value: 0;
    }
    
    .progress {
      margin: 20px 0;
      height: 10px;
      position: relative;
      background: linear-gradient(lightgreen 0 0) 0/calc(var(--p,0)*1%) 100% no-repeat blue;
      animation:p1 1s 1s both;
      --s:var(--p);
      counter-set:num var(--s);
    }
    
    .progress::before {
      content: counter(num) "%";
      font-family: monospace;
      font-size:20px;
      white-space:nowrap;
      overflow: hidden;
      position: absolute;
      background: rgba(255, 0, 0, 0.8);
      border:5px solid transparent;
      top:50%;
      left: calc(var(--p)*1%);
      transform: translate(calc(-1%*var(--p)),-50%);
      color: #FFF;
    }
    @keyframes p1 {from {--p:0;--s:0}}
    <div class="progress" style="--p:0"></div>
    
    <div class="progress" style="--p:20"></div>
    
    <div class="progress" style="--p:50"></div>
    
    
    <div class="progress" style="--p:80"></div>
    
    <div class="progress" style="--p:100"></div>

    Until there is more support, you can fake it like below:

    body {
      margin: 100px;
      background: #CCC
    }
    
    .progress {
      margin: 20px 0;
      height: 10px;
      position: relative;
      background: linear-gradient(lightgreen 0 0) 0/var(--p) 100% no-repeat blue;
      animation:p1 1s 1s both;
    }
    
    .progress::before {
      content: attr(style);
      font-family: monospace;
      font-size:20px;
      white-space:nowrap;
      text-indent: -4ch;
      overflow: hidden;
      position: absolute;
      background: rgba(255, 0, 0, 0.8);
      border:5px solid transparent;
      top:50%;
      left: var(--p);
      transform: translate(calc(-1*var(--p)),-50%);
      color: #FFF;
      animation:p2 1s 1s both,p3 0.8s 1s both;
    }
    @keyframes p1 {from {background-size:0% 100%}}
    @keyframes p2 {from {left:0%;transform: translate(0%,-50%)}}
    @keyframes p3 { /* put some randome number to fake the animation*/
      0%  {content:"--p:0%"}
      15% {content:"--p:5%"}
      30% {content:"--p:9%"}
      45% {content:"--p:10%"}
      60% {content:"--p:11%"}
      75% {content:"--p:40%"}
      90% {content:"--p:20%"}
    }
    <div class="progress" style="--p:0%"></div>
    
    <div class="progress" style="--p:20%"></div>
    
    <div class="progress" style="--p:50%"></div>
    
    
    <div class="progress" style="--p:80%"></div>
    
    <div class="progress" style="--p:100%"></div>

    Or some crazy idea like below:

    body {
      margin: 100px;
      background: #CCC
    }
    
    .progress {
      margin: 20px 0;
      height: 10px;
      position: relative;
      background: linear-gradient(lightgreen 0 0) 0/calc(var(--p)*1%) 100% no-repeat blue;
      animation:p1 1s 1s both;
    }
    
    .progress::before {
      content: "0% \A 1% \A 2% \A 3% \A 4% \A 5% \A 6% \A 7% \A 8% \A 9% \A 10% \A 11% \A 12% \A 13% \A 14% \A 15% \A 16% \A 17% \A 18% \A 19% \A 20% \A 21% \A 22% \A 23% \A 24% \A 25% \A 26% \A 27% \A 28% \A 29% \A 30% \A 31% \A 32% \A 33% \A 34% \A 35% \A 36% \A 37% \A 38% \A 39% \A 40% \A 41% \A 42% \A 43% \A 44% \A 45% \A 46% \A 47% \A 48% \A 49% \A 50% \A 51% \A 52% \A 53% \A 54% \A 55% \A 56% \A 57% \A 58% \A 59% \A 60% \A 61% \A 62% \A 63% \A 64% \A 65% \A 66% \A 67% \A 68% \A 69% \A 70% \A 71% \A 72% \A 73% \A 74% \A 75% \A 76% \A 77% \A 78% \A 79% \A 80% \A 81% \A 82% \A 83% \A 84% \A 85% \A 86% \A 87% \A 88% \A 89% \A 90% \A 91% \A 92% \A 93% \A 94% \A 95% \A 96% \A 97% \A 98% \A 99% \A 100%";
      font-family: monospace;
      font-size:20px;
      width:4ch;
      line-height:1em;
      height:1em;
      text-align:center;
      overflow: hidden;
      position: absolute;
      background: rgba(255, 0, 0, 0.8);
      border:5px solid transparent;
      top:50%;
      left: calc(var(--p)*1%);
      transform: translate(calc(-1%*var(--p)),-50%);
      color: #0000;
      text-shadow:0 calc(var(--p)*-1em) 0 #fff;
      animation:p2 1s 1s both,p3 1s 1s steps(var(--p)) both;
    }
    @keyframes p1 {from {background-size:0% 100%}}
    @keyframes p2 {from {left:0%;transform: translate(0%,-50%)}}
    @keyframes p3 {from {text-shadow:0 0 0 #fff}}
    <div class="progress" style="--p:0"></div>
    
    <div class="progress" style="--p:20"></div>
    
    <div class="progress" style="--p:50"></div>
    
    <div class="progress" style="--p:80"></div>
    
    <div class="progress" style="--p:100"></div>