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 basically how I want it to look. Is it possible with a reasonably simple solution?
.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>
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`
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>