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

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; } } }