How to Make a Range Sliders With a Rolling Counter in HTML5 & CSS3

How to Make a Range Sliders With a Rolling Counter in HTML5 & CSS3

 

<form>
	<label for="range1">One Digit</label>
	<input id="range1" name="range1" type="range" min="0" max="9" value="5">
	<label for="range2">More Digits</label>
	<input id="range2" name="range2" type="range" min="0" max="300" value="150">
</form>

 

* {
	border: 0;
	box-sizing: border-box;
	margin: 0;
	padding: 0;
}
: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));
}
body, input {
	color: var(--fg);
	font: 1em/1.5 "Hind", sans-serif;
}
body, .range, .range__counter {
	display: flex;
}
body {
	background-color: var(--bg);
	height: 100vh;
}
form, input, .range__input, .range__counter-sr {
	width: 100%;
}
form {
	margin: auto;
	padding: 0 0.75em;
	max-width: 17em;
}
label {
	font-weight: bold;
}
.range:not(:last-child) {
	margin-bottom: 1.5em;
}
.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;
	}
}

 

window.addEventListener("DOMContentLoaded",() => {
	let range1 = new RollCounterRange("#range1"),
		range2 = new RollCounterRange("#range2");
});

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

 

Leave a Reply