Search code examples
javascripthtmlcssprogress-bar

"Progress" bar with both value and acceptable ranges


Here is exhaustive topic on SO about how to create progress bar. I would like to improve this "widget" to display acceptable range markers. It may be vertical lines or something else.

For example, value range may be [-50;50], but acceptable range is [-25;25]. So can someone point me out how to modify, for example, the first answer from topic mentioned above to get what I described here.

Here is first suggested answer from the topic:

#progressbar {
  background-color: black;
  border-radius: 13px;
  /* (height of inner div) / 2 + padding */
  padding: 3px;
}

#progressbar>div {
  background-color: orange;
  width: 40%;
  /* Adjust with JavaScript */
  height: 20px;
  border-radius: 10px;
}
<div id="progressbar">
  <div></div>
</div>

Here is how I see my widget. Red parts of bar - acceptable range.

enter image description here


Solution

  • Clarification

    So firstly, as mentioned in my comment, this doesn't really sound like a progress bar. As implied by the name, progress bars are meant to show progress, and so things like negative values don't make sense.

    It sounds like you want something like the HTML Range Input, though you mentioned you only want to display data (which you could still technically do by setting the disabled attribute on a range input).

    Possible Solution

    Ultimately it looks like you just want CSS to display a range (not a progress bar). This can be achieved with pure CSS, but I should mention there are a few quirks based on the requirements you have outlined.

    You could set all the values by hand, based on whatever range and value you wish to display, but I assume this isn't desirable. So the next thing to do would be to utilize CSS variables and the CSS calc() function to set everything for you (based on some initial data).

    The one weird thing is displaying the text for things like the range and values. Because we are using CSS variables to hold our values and perform calculations, it would be nice to use those same values to display the text. But CSS variables cannot be converted between types and so a value of say 2 is a number (not text or a string), and this means the value of 2 cannot be displayed as text using the CSS content property. Because of this I have 2 sets of variables. The first set is the number, used for calculations to set the widths. The second set is the -text version, used to display the text under your range bar.

    .rangeBar {
      background: #EEE;
      height: 2em;
      padding: .2em;
      border: 1px solid #CCC;
      border-radius: 1em;
      box-sizing: border-box;
      position: relative;
      --min-value: 0;
      --min-value-text: '0';
      --max-value: 4.5;
      --max-value-text: '4.5';
      --min-range: 1;
      --min-range-text: '1';
      --max-range: 3;
      --max-range-text: '3';
      --value: 2;
      --value-text: '2';
    }
    .rangeBar::before {
      content: var(--min-value-text);
      position: absolute;
      top: 100%;
      left: 0;
      color: #888;
    }
    .rangeBar::after {
      content: var(--max-value-text);
      position: absolute;
      top: 100%;
      right: 0;
      color: #888;
    }
    
    .rangeBar .value {
      background: #0A95FF;
      width: calc(var(--value)/var(--max-value)*100%);
      height: 100%;
      border-radius: 1em;
      position: relative;
      z-index: 1;
    }
    .rangeBar .value::after {
      content: var(--value-text);
      position: absolute;
      top: 100%;
      right: 0;
      color: #888;
    }
    
    .rangeBar .minRange {
      background: #E74C3C;
      width: calc(var(--min-range)/var(--max-value)*100%);
      height: 100%;
      border-radius: 1em;
      border-top-right-radius: 0;
      border-bottom-right-radius: 0;
      position: absolute;
      top: 0;
      left: 0;
      z-index: 2;
    }
    .rangeBar .minRange::after {
      content: var(--min-range-text);
      position: absolute;
      top: 100%;
      right: 0;
      color: #888;
    }
    
    .rangeBar .maxRange {
      background: #E74C3C;
      width: calc((var(--max-value) - var(--max-range))/var(--max-value)*100%);
      height: 100%;
      border-radius: 1em;
      border-top-left-radius: 0;
      border-bottom-left-radius: 0;
      position: absolute;
      top: 0;
      right: 0;
      z-index: 2;
    }
    .rangeBar .maxRange::after {
      content: var(--max-range-text);
      position: absolute;
      top: 100%;
      left: 0;
      color: #888;
    }
    <div class="rangeBar">
      <div class="minRange"></div>
      <div class="value"></div>
      <div class="maxRange"></div>
    </div>

    Additional Notes

    There are possibly a few ways to simplify the CSS for this and automatically take care of some of the issues with this, but would require JavaScript (which is outside of the scope of this question). There has been no indication as to how any of the data or values for this range bar will be set, and so JavaScript was avoided for this question.

    EDIT

    Because OP updated the original question to include JavaScript, I am adding an additional solution. This mostly works the same but instead uses a JavaScript function called _CreateRange that takes 5 parameters (min value, max value, min range, max range, and value) and creates a new element on the page that uses those parameters/values. This makes things a little simpler as you only need to enter those values once (rather than once for the number value and once for the text value) and you can also use this to dynamically create or load ranges on the page (depending on where the data for these ranges is coming from).

    // These are just example values you can modify
    let value = 2,
        minValue = 0,
        maxValue = 4.5,
        minRange = 1,
        maxRange = 3;
    
    const _CreateRange = (mnV, mxV, mnR, mxR, v) => {
      let r = document.createElement("div");
      r.className = "rangeBar";
      r.innerHTML = `<div class="minRange"></div><div class="value"></div><div class="maxRange"></div>`;
      r.style.setProperty("--min-value", mnV);
      r.style.setProperty("--min-value-text", JSON.stringify(mnV+""));
      r.style.setProperty("--max-value", mxV);
      r.style.setProperty("--max-value-text", JSON.stringify(mxV+""));
      r.style.setProperty("--min-range", mnR);
      r.style.setProperty("--min-range-text", JSON.stringify(mnR+""));
      r.style.setProperty("--max-range", mxR);
      r.style.setProperty("--max-range-text", JSON.stringify(mxR+""));
      r.style.setProperty("--value", v);
      r.style.setProperty("--value-text", JSON.stringify(v+""));
      
      document.querySelector("#bar").append(r);
    }
    
    // This is where the function to create the range is called
    // We are using our default example values from earlier, but you can pass in any values
    _CreateRange(minValue, maxValue, minRange, maxRange, value);
    .rangeBar {
      background: #EEE;
      height: 2em;
      padding: .2em;
      border: 1px solid #CCC;
      box-sizing: border-box;
      position: relative;
      margin: 0 0 2em;
    }
    .rangeBar::before {
      content: var(--min-value-text);
      position: absolute;
      top: 100%;
      left: 0;
      color: #888;
    }
    .rangeBar::after {
      content: var(--max-value-text);
      position: absolute;
      top: 100%;
      right: 0;
      color: #888;
    }
    
    .rangeBar .value {
      background: #0A95FF;
      width: calc(var(--value)/var(--max-value)*100%);
      height: 100%;
      position: relative;
      z-index: 1;
    }
    .rangeBar .value::after {
      content: var(--value-text);
      position: absolute;
      top: 100%;
      right: 0;
      color: #888;
      margin: .2em 0 0;
    }
    
    .rangeBar .minRange {
      background: #E74C3C;
      width: calc(var(--min-range)/var(--max-value)*100%);
      height: 100%;
      border-top-right-radius: 0;
      border-bottom-right-radius: 0;
      position: absolute;
      top: 0;
      left: 0;
      z-index: 2;
    }
    .rangeBar .minRange::after {
      content: var(--min-range-text);
      position: absolute;
      top: 100%;
      right: 0;
      color: #888;
    }
    
    .rangeBar .maxRange {
      background: #E74C3C;
      width: calc((var(--max-value) - var(--max-range))/var(--max-value)*100%);
      height: 100%;
      border-top-left-radius: 0;
      border-bottom-left-radius: 0;
      position: absolute;
      top: 0;
      right: 0;
      z-index: 2;
    }
    .rangeBar .maxRange::after {
      content: var(--max-range-text);
      position: absolute;
      top: 100%;
      left: 0;
      color: #888;
    }
    <div id="bar"></div>