htmlcssprogress-barborder-radius

How to partially use a border-radius with an actual border in a progress bar


I have multiple segments in a progress bar, in this example representing two states - achieved and planned values. The planned segment should have a border and nearing the end of the progress bar it should get the same border-radius as the background.

My problem is the following. Either the the radius is also visible within the progress bar, or I don't use a border-radius for the right corner and use overflow: hidden; to "cut off" the unwanted overflowing part of the segment but then I lose the border on the radius.

Difficult to explain, images are probably better.

This is my current output: output screenshot

This is basically how I want it to look. Is it possible with a reasonably simple solution?

output screenshot

.progress-bar {
  position: relative;
  height: 40px;
  width: 400px;
  background: lightgray;
  border-radius: 24px;
  .segment {
    position: absolute;
    height: 40px;
    box-sizing: border-box;
    &:first-of-type {
      border-radius: 24px 0 0 24px;
      width: 70%;
      background: blue;
    }
    &:last-of-type {
      left: 70%;
      width: 28%;
      background: lightblue;
      border: 1px solid blue;
    }
  }
}
<div class="progress-bar">
  <div class="segment"></div>
  <div class="segment"></div>
</div>


Solution

  • You could move your border to the bottom layered progress-bar and then use a clip-path on the segments. Then if you ever want to go 100% the border-radius will still be present. Using CSS variables will allow dynamic calculations provided you want to exclude the radius from the percentage.

    The clip-path - inset function defines a rectangle at the specified inset distances from each side of the reference box. So if you want to take up 70% with the first segment set the right side to 30% provided your width is 100%. Then on the :last-of-type segment if you want it to stop shy of the border-radius, calc the left side to be 100% - border-radius.

    I included a few examples and how the inset could be set for differing percentages.

    body {
      --br: 20px;
      --height: 40px;
      font-family: sans-serif;
    }
    
    .calcs {
      margin-left: 0.5rem;
    }
    
    .segment {
      padding-left: 2rem;
      color: white;
    }
    
    .segment:last-of-type {
      text-align: right;
      padding-right: 6rem;
      color: blue;
    }
    
    .progress-bar {
      position: relative;
      height: var(--height);
      width: 400px;
      background: lightgray;
      border-radius: var(--br);
      border: 1px solid blue;
      .segment {
        padding-right: var(--br);
        position: absolute;
        height: var(--height);
        box-sizing: border-box;
        &:first-of-type {
          border-radius: var(--br) 0 0 var(--br);
          width: 100%;
          clip-path: inset(0% 30% 0% 0%);
          /* inset top, right, bottom, and left */
          background: blue;
        }
        &:last-of-type {
          width: 100%;
          /* make the width 100% */
          background: lightblue;
          /* inset of 40% from right with a width of 100% - half the height */
          clip-path: inset(0% 40% 0% calc(100% - var(--height)/2));
          /* inset: top, right, bottom, and left */
        }
      }
    }
    
    .progress-bar2 {
      position: relative;
      height: var(--height);
      width: 400px;
      background: lightgray;
      border-radius: var(--br);
      border: 1px solid blue;
      /* place your colored border-radius here */
      .segment {
        position: absolute;
        height: var(--height);
        box-sizing: border-box;
        &:first-of-type {
          border-radius: var(--br) 0 0 var(--br);
          width: 100%;
          clip-path: inset(0% 30% 0% 0%);
          /* inset: top, right, bottom, and left */
          background: blue;
        }
        &:last-of-type {
          width: 100%;
          /* make the width 100% */
          background: lightblue;
          /* inset of 30% from right with a width of 100%  */
          clip-path: inset(0% 50% 0% 90%);
          /* inset: top, right, bottom, and left */
        }
      }
    }
    
    .progress-bar3 {
      position: relative;
      height: var(--height);
      width: 400px;
      background: lightgray;
      border-radius: var(--br);
      border: 1px solid blue;
      .segment {
        position: absolute;
        height: var(--height);
        box-sizing: border-box;
        &:first-of-type {
          border-radius: 24px 0 0 24px;
          width: 100%;
          clip-path: inset(0% 70% 0% 0%);
          /* inset top, right, bottom, and left */
          background: blue;
        }
        &:last-of-type {
          width: 100%;
          /* make the width 100% */
          background: lightblue;
          /* inset of 30% from right with a width of 100%  */
          clip-path: inset(0% 70% 0% 100%);
          /* inset top, right, bottom, and left */
          border-radius: var(--br);
          /* giving the width 100% and border radius on end means 
                                           clip-path will have a radius on the end if 100% */
        }
      }
    }
    
    .progress-bar4 {
      position: relative;
      height: var(--height);
      width: 400px;
      background: lightgray;
      border-radius: var(--br);
      border: 1px solid blue;
      .segment {
        position: absolute;
        height: var(--height);
        box-sizing: border-box;
        &:first-of-type {
          border-radius: 24px 0 0 24px;
          width: 100%;
          clip-path: inset(0% 0% 0% 0%);
          /* inset top, right, bottom, and left */
          background: blue;
          border-radius: var(--br);
          /* giving the width 100% and border radius on end means 
             clip-path will have a radius on the end if 100% */
        }
        &:last-of-type {
          width: 100%;
          /* make the width 100% */
          background: lightblue;
          /* inset of 30% from right with a width of 100%  */
          clip-path: inset(0% 0% 0% 100%);
          /* inset top, right, bottom, and left */
          border-radius: var(--br);
          /* giving the width 100% and border radius on end means 
             clip-path will have a radius on the end if 100% */
        }
      }
    }
    <p class="calcs"><em>last-of-type</em> inset: right - <strong>60%</strong> (:) left - 34% => <strong>calc(100% - 40px/2)</strong> = appx: 94% total</p>
    <div class="progress-bar">
      <div class="segment">60%</div>
      <div class="segment">40% - border<br>radius</div>
    </div>
    <p class="calcs"><em>last-of-type</em> inset: right - <strong>50%</strong> (:) left - 40% => <strong>90%</strong> total</p>
    <div class="progress-bar2">
      <div class="segment">50%</div>
      <div class="segment">40%</div>
    </div>
    <p class="calcs"><em>last-of-type</em> inset: right - <strong>30%</strong> (:) left - 70% => <strong>100%</strong> total</p>
    <div class="progress-bar3">
      <div class="segment">30%</div>
      <div class="segment">70%</div>
    </div>
    <p class="calcs"><em>last-of-type</em> inset: right - <strong>0%</strong> (:) left - 0% => <strong>100%</strong> total</p>
    <div class="progress-bar4">
      <div class="segment">100%</div>
      <div class="segment">0</div>
    </div>

    EDIT: With some javascript you can calculate the distance from center of radius on both X and Y axis to get the offset amount using an arc elipsis as described in MDN for border-top-right-radius and border-top-left-radius.

    By controlling your unit lengths in CSS using variables, you can set these properties after calculating the distances from the radius of your border-radius property.

    Get the width of all elements and then use them to calculate the end of the segment elements in relation to the progress-bar element (left over length). Then take that number and subtract it from the border-radius amount to get an offset from the radius center. Then we use that to find the intersecting height of the y axis line from radius.

    Once we have the y axis offset length and the x axis offset length, we can set the border radius ellipses using javascript to set the style of the last-of-type elements border radius' on the right side.

    segment[1].style.borderTopRightRadius = `${xAxis}px ${yAxis}px`
    segment[1].style.borderBottomRightRadius = `${xAxis}px ${yAxis}px`
    

    Math.pow

    Math.sqrt

    I used an allowance for the border width in a switch to accommodate differing calculated percentages.

    // get the progress bar element
    const progressBar = document.querySelectorAll('.progress-bar');
    
    // method to detect the line length of the intersecting y axis line
    // from center traveling the offset distance along the x axis
    // returns the length of the vertical line as it intersects the outer radius
    function findIntersectionHeight(radius, xOffset) {
      // returns the square root of (radius to the 2nd power minus xOffset to the 2nd power)
      return Math.sqrt(Math.pow(radius, 2) - Math.pow(xOffset, 2));
    }
    
    // loop over the progress bar elements 
    // get the index for display purposes in p tag
    progressBar.forEach((bar,i) => {
      // query the segment elements within each progress-bar 
      const segment = bar.querySelectorAll('.segment');
      // for display purposes only
      const percentage = document.querySelectorAll('.percentage');
      // set css variables for first-of-type and last-of-type
      // using the dataset attributes to set lengths in progress-bar
      bar.style.setProperty('--first-width', `${segment[0].dataset.width}%`);
      bar.style.setProperty('--second-width', `${segment[1].dataset.width}%`);
      // for display purposes only
      percentage[i].innerHTML = `first-of-type percentage = ${segment[0].dataset.width}% <br> last-of-type percentage = ${segment[1].dataset.width}% <br> Total percentage = ${Number(segment[0].dataset.width) + Number(segment[1].dataset.width)}%`;
      
      // get the boundingClientRect for each element
      const barRect = bar.getBoundingClientRect();
      const firstSegRect = segment[0].getBoundingClientRect();
      const secondSegRect = segment[1].getBoundingClientRect();
      
      // set an allowance for a sliding percentage when inside
      // border radius, this accommodates the border width itself
      let borderAllowance = 2;
      switch ((secondSegRect.width + firstSegRect.width) / barRect.width * 100) {
        case 99:
          borderAllowance = 1;
          break;
        case 98:
          borderAllowance = 1.336;
          break;
        case 97:
          borderAllowance = 1.667;
          break;
      }
    
      // get the progress-bars border radius and remove px unit length and subtract border allowance
      const barRadius = getComputedStyle(bar).borderRadius.split('px')[0] - borderAllowance;
      
      // calculate the length of the parent minus the combined length of the children minus the border allowance
      const xAxisOffset = barRect.width - (secondSegRect.width + firstSegRect.width) - borderAllowance;
      
      // calculate the distance from radius center to travel on xAxis
      const xAxis = barRadius - xAxisOffset + borderAllowance;
      
      // method to get the intersecting y axis line length of the vetical offset
      // to the outside radius from the center of the x axis within the radius
      const yAxisOffset = findIntersectionHeight(barRadius, xAxis);
      
      // calculate the remaining distance left over
      const yAxis = barRadius - yAxisOffset;
    
      // set the border corner properties using a css variable
      segment[1].style.borderTopRightRadius = `${xAxis}px ${yAxis}px`;
      segment[1].style.borderBottomRightRadius = `${xAxis}px ${yAxis}px`;
    
    })
    .progress-bar {
      --parent-width: 400px;
      --parent-height: 40px;
      --br: calc(var(--parent-height) / 2);
      position: relative;
      height: var(--parent-height);
      width: var(--parent-width);
      background: lightgray;
      border-radius: var(--br);
      overflow: hidden; /* hide the overflow of the children */  
      .segment {
        position: absolute;
        height: var(--parent-height);
        box-sizing: border-box;
        &:first-of-type {
          border-radius: var(--br) 0 0 var(--br);
          width: var(--first-width);
          background: blue;
        }
        &:last-of-type {
          left: var(--first-width);
          width: var(--second-width);
          background: lightblue;
          border: 1px solid blue;
        }
      }
    }
    <p class="percentage"></p>
    <div class="progress-bar">
      <div class="segment" data-width="80"></div>
      <div class="segment" data-width="16"></div>
    </div>
    <p class="percentage"></p>
    <div class="progress-bar">
      <div class="segment" data-width="50"></div>
      <div class="segment" data-width="47"></div>
    </div>
    <p class="percentage"></p>
    <div class="progress-bar">
      <div class="segment" data-width="70"></div>
      <div class="segment" data-width="28"></div>
    </div>
    <p class="percentage"></p>
    <div class="progress-bar">
      <div class="segment" data-width="30"></div>
      <div class="segment" data-width="69"></div>
    </div>