Modern Range Slider With Rolling Counter

Category: Form , Javascript | June 5, 2020
Author:Jon Kantner
Views Total:6,680 views
Official Page:Go to website
Last Update:June 5, 2020
License:MIT

Preview:

Modern Range Slider With Rolling Counter

Description:

A JavaScript/CSS solution to create a modern range slider control with a rolling counter representing the current value you picked.

See It In Action:

See the Pen
Range Sliders With a Rolling Counter
by Jon Kantner (@jkantner)
on CodePen.

How to use it:

1. Create a normal range slider input on the page.

<input id="range" name="range" type="range" min="0" max="100" value="15" />

2. The necessary CSS styles for the range slider, slider/range thumbs, roller counter, etc.

/* Reset */
* {
  border: 0;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
/* Ovrrride Variables Here */
:root {
  --bg: #e3e4e8;
  --bgT: #e3e4e800;
  --fg: #17181c;
  --inputBg: #fff;
  --handleBg: #255ff4;
  --handleDownBg: #0b46da;
  --handleTrackBg: #5583f6;
  font-size: calc(16px + (32 - 16)*(100vw - 320px)/(2560 - 320));
}
/* Core */
input {
  color: var(--fg);
}
.range, .range__counter {
  display: flex;
}
input, .range__input, .range__counter-sr {
  width: 100%;
}
.range input[type=range],
.range input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none; 
  appearance: none;
}
.range input[type=range], .range__input-fill {
  border-radius: 0.25em;
  height: 0.5em;
}
.range input[type=range] {
  background-color: var(--inputBg);
  display: block;
  margin: 0.5em 0;
  padding: 0;
}
.range input[type=range]:focus {
  outline: transparent;
}
.range input[type=range]::-webkit-slider-thumb {
  background-color: var(--handleBg);
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  position: relative;
  transition: background 0.1s linear;
  width: 1.5em;
  height: 1.5em;
  z-index: 1;
}
.range input[type=range]::-moz-range-thumb {
  background-color: var(--handleBg);
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  position: relative;
  transform: translateZ(1px);
  transition: background-color 0.1s linear;
  width: 1.5em;
  height: 1.5em;
  z-index: 1;
}
.range input[type=range]::-moz-focus-outer {
  border: 0;
}
.range__input, .range__input-fill, .range__counter-column, .range__counter-digit {
  display: block;
}
.range__input, .range__counter {
  position: relative;
}
.range__input {
  margin-right: 0.375em;
}
.range__input:active input[type=range]::-webkit-slider-thumb,
.range input[type=range]:focus::-webkit-slider-thumb,
.range input[type=range]::-webkit-slider-thumb:hover {
  background-color: var(--handleDownBg);
}
.range__input:active input[type=range]::-moz-range-thumb,
.range input[type=range]:focus::-moz-range-thumb,
.range input[type=range]::-moz-range-thumb:hover {
  background-color: var(--handleDownBg);
}
.range__input-fill, .range__counter-sr {
  position: absolute;
  left: 0;
}
.range__input-fill {
  background-color: var(--handleTrackBg);
  pointer-events: none;
  top: calc(50% - 0.25em);
}
.range__counter, .range__counter-digit {
  height: 1.5em;
}
.range__counter {
  margin: auto 0;
  overflow: hidden;
  text-align: center;
}
.range__counter-sr {
  background-image: linear-gradient(var(--bg),var(--bgT) 0.3em 1.2em,var(--bg));
  color: transparent;
  letter-spacing: 0.06em;
  top: 0;
  text-align: right;
  z-index: 1;
}
.range__counter-column {
  transition: transform 0.25s ease-in-out;
  width: 0.66em;
  -webkit-user-select: none;
  -moz-user-select: none;
  user-select: none;
}
.range__counter-column--pause {
  transition: none;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #2e3138;
    --bgT: #2e313800;
    --fg: #e3e4e8;
    --inputBg: #17181c;
  }
}

3. The core JavaScript to enable the range slider & rolling counter.

window.addEventListener("DOMContentLoaded",() => {
  let range = new RollCounterRange("#range"),
});
class RollCounterRange {
  constructor(id) {
    this.el = document.querySelector(id);
    this.srValue = null;
    this.fill = null;
    this.digitCols = null;
    this.lastDigits = "";
    this.rollDuration = 0; // the transition duration from CSS will override this
    this.trans09 = false;
    if (this.el) {
      this.buildSlider();
      this.el.addEventListener("input",this.changeValue.bind(this));
    }
  }
  buildSlider() {
    // create a div to contain the <input>
    let rangeWrap = document.createElement("div");
    rangeWrap.className = "range";
    this.el.parentElement.insertBefore(rangeWrap,this.el);
    // create another div to contain the <input> and fill
    let rangeInput = document.createElement("span");
    rangeInput.className = "range__input";
    rangeWrap.appendChild(rangeInput);
    // range fill, place the <input> and fill inside container <span>
    let rangeFill = document.createElement("span");
    rangeFill.className = "range__input-fill";
    rangeInput.appendChild(this.el);
    rangeInput.appendChild(rangeFill);
    // create the counter
    let counter = document.createElement("span");
    counter.className = "range__counter";
    rangeWrap.appendChild(counter);
    // screen reader value
    let srValue = document.createElement("span");
    srValue.className = "range__counter-sr";
    srValue.textContent = "0";
    counter.appendChild(srValue);
    // column for each digit
    for (let D of this.el.max.split("")) {
      let digitCol = document.createElement("span");
      digitCol.className = "range__counter-column";
      digitCol.setAttribute("aria-hidden","true");
      counter.appendChild(digitCol);
      // digits (blank, 0–9, fake 0)
      for (let d = 0; d <= 11; ++d) {
        let digit = document.createElement("span");
        digit.className = "range__counter-digit";
        if (d > 0)
          digit.textContent = d == 11 ? 0 : `${d - 1}`;
        digitCol.appendChild(digit);
      }
    }
    this.srValue = srValue;
    this.fill = rangeFill;
    this.digitCols = counter.querySelectorAll(".range__counter-column");
    this.lastDigits = this.el.value;
    while (this.lastDigits.length < this.digitCols.length)
      this.lastDigits = " " + this.lastDigits;
    this.changeValue();
    // use the transition duration from CSS
    let colCS = window.getComputedStyle(this.digitCols[0]),
      transDur = colCS.getPropertyValue("transition-duration"),
      msLabelPos = transDur.indexOf("ms"),
      sLabelPos = transDur.indexOf("s");
    if (msLabelPos > -1)
      this.rollDuration = transDur.substr(0,msLabelPos);
    else if (sLabelPos > -1)
      this.rollDuration = transDur.substr(0,sLabelPos) * 1e3;
  }
  changeValue() {
    // keep the value within range
    if (+this.el.value > this.el.max)
      this.el.value = this.el.max;
    else if (+this.el.value < this.el.min)
      this.el.value = this.el.min;
    // update the screen reader value
    if (this.srValue)
      this.srValue.textContent = this.el.value;
    // width of fill
    if (this.fill) {
      let pct = this.el.value / this.el.max,
        fillWidth = pct * 100,
        thumbEm = 1 - pct;
      this.fill.style.width = `calc(${fillWidth}% + ${thumbEm}em)`;
    }
    if (this.digitCols) {
      let rangeVal = this.el.value;
      // add blanks at the start if needed
      while (rangeVal.length < this.digitCols.length)
        rangeVal = " " + rangeVal;
      // get the differences between current and last digits
      let diffsFromLast = [];
      if (this.lastDigits) {
        rangeVal.split("").forEach((r,i) => {
          let diff = +r - this.lastDigits[i];
          diffsFromLast.push(diff);
        });
      }
      // roll the digits
      this.trans09 = false;
      rangeVal.split("").forEach((e,i) => {
        let digitH = 1.5,
          over9 = false,
          under0 = false,
          transY = e === " " ? 0 : (-digitH * (+e + 1)),
          col = this.digitCols[i];
        // start handling the 9-to-0 or 0-to-9 transition
        if (e == 0 && diffsFromLast[i] == -9) {
          transY = -digitH * 11;
          over9 = true;
        } else if (e == 9 && diffsFromLast[i] == 9) {
          transY = 0;
          under0 = true;
        }
        col.style.transform = `translateY(${transY}em)`;
        col.firstChild.textContent = "";
        // finish the transition
        if (over9 || under0) {
          this.trans09 = true;
          // add a temporary 9
          if (under0)
            col.firstChild.textContent = e;
          setTimeout(() => {
            if (this.trans09) {
              let pauseClass = "range__counter-column--pause",
                transYAgain = -digitH * (over9 ? 1 : 10);
              col.classList.add(pauseClass);
              col.style.transform = `translateY(${transYAgain}em)`;
              void col.offsetHeight;
              col.classList.remove(pauseClass);
              // remove the 9
              if (under0)
                col.firstChild.textContent = "";
            }
          },this.rollDuration);
        }
      });
      this.lastDigits = rangeVal;
    }
  }
}

You Might Be Interested In:


Leave a Reply