Came across this tutorial Let’s Make a Multi-Thumb Slider That Calculates The Width Between Thumbs which was written in React and TypeScript. Decided to convert it to vanilla (plain) CSS and JavaScript 🙂
First up, a working demo below. Move the slider thumbs and an event will be emitted, which will be output to the console and displayed below the slider as well.
Source code:
<style> #notes { font-family: monospace; width: 100%; word-wrap: break-word; } .slider { background-color: transparent; display: flex; width: 100%; } .tag { border-left-width: 0.1em; border-left-style: solid; border-left-color: white; border-right-width: 0.1em; border-right-style: solid; border-right-color: white; box-sizing: border-box; padding: 0.5em; position: relative; text-align: center; } .tag-text { color: white; display: block; font-family: sans-serif; font-weight: bold; overflow: hidden; user-select: none; } .tag:first-of-type { border-radius: 4px 0px 0px 4px; } .tag:last-of-type { border-radius: 0px 4px 4px 0px; } .tag:last-of-type > .slider-thumb { display: none !important; } .slider-thumb { align-items: center; background-color: white; border-radius: 2em; bottom: 0; cursor: ew-resize; display: flex; height: 2em; justify-content: center; margin: auto; width: 2em; position: absolute; right: calc(-1.1em); top: 0; user-select: none; z-index: 10; } .slider-thumb span { font-size: 1.5em; font-weight: bold; } </style> <div class="slider"></div> <div id="notes"></div><br> <script> let sliderElement = document.querySelector('.slider'); let notesElement = document.getElementById('notes'); sliderElement.addEventListener('demo.slider.tags', (event) => { let text = 'Updated tag values:' + JSON.stringify(event.detail.tags); console.log(text); notesElement.innerHTML = text; }); // Adapted into vanilla CSS/JavaScript from React/TypeScript in article // https://newsakmi.com/news/tech-news/software/lets-make-a-multi-thumb-slider-that-calculates-the-width-between-thumbs // Unlike the original article, the tags here are not removed if value becomes 0. const demoScript = (function (sliderElement) { /** * Self reference - all public properties/methods are stored here and returned as public interface * * @public * @type {object} */ const self = {}; /** * Tags, i.e. sections, in slider * * At any one time, the sum of values for all the tags will always be * the sum of the initial values here, and not fixed at 100 (percent). * The values shown in this demo just happen to add up to 100. * * @private * @type {object[]} */ let tags = [ { name: 'Sky', color: 'red', value: 70 }, { name: 'Earth', color: 'green', value: 20 }, { name: 'Ocean', color: 'blue', value: 10 }, ]; window.addEventListener('DOMContentLoaded', (event) => { let html = ''; tags.forEach((tag, index) => { // Using pointerdown instead of mousedown to capture both mouse and touchscreen // events. See // https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onpointerdown // for more info. html += ` <div class="tag" data-tag-index="${index}" style="background:${tag.color}; width:${tag.value}%;"> <span class="tag-text tag-name">${tag.name}</span> <span class="tag-text tag-value">${tag.value}%</span> <div class="slider-thumb" onpointerdown="demoScript.onSliderSelect(event, this);"><span>⬌</span></div> </div> `; }); sliderElement.innerHTML = html; }); /** * Handler when user clicks on slider thumb * * @param {Event} event * @returns {void} */ self.onSliderSelect = function (event, sliderThumb) { event.preventDefault(); document.body.style.cursor = 'ew-resize'; let tagIndex = parseInt(sliderThumb.parentNode.getAttribute('data-tag-index')); let tag = tags[tagIndex]; let startDragX = event.pageX; let sliderWidth = sliderElement.offsetWidth; let values = tags.map((tag) => tag.value); // initial values // The onResize and onEventUp listeners are specific to each // onSliderSelect event hence created inside let onResize = function (event) { event.preventDefault(); let endDragX = event.touches ? event.touches[0].pageX : event.pageX; let distanceMoved = endDragX - startDragX; values = tags.map((tag) => tag.value); // read in updated values let maxValue = values[tagIndex] + values[tagIndex + 1]; let valueMoved = nearestMultiple(1, getPercent(sliderWidth, distanceMoved)); let prevValue = values[tagIndex]; let newValue = prevValue + valueMoved; let currTagValue = clamp(newValue, 0, maxValue); values[tagIndex] = currTagValue; let nextTagIndex = tagIndex + 1; let nextTagNewValue = values[nextTagIndex] - valueMoved; let nextTagValue = clamp(nextTagNewValue, 0, maxValue); values[nextTagIndex] = nextTagValue; // Update slider Array.from(document.querySelectorAll('.tag')).forEach((tagElement) => { let tagIndex = parseInt(tagElement.getAttribute('data-tag-index')); let value = values[tagIndex]; tagElement.style.width = value + '%'; tagElement.querySelector('.tag-value').innerHTML = value + '%'; }); }; let removeEventListener = function () { window.removeEventListener('pointermove', onResize); window.removeEventListener('touchmove', onResize); // Update values only when user stops moving slider thumb tags.forEach((tag, tagIndex) => { tag.value = values[tagIndex]; }); emitEvent(); }; let onEventUp = function (event) { event.preventDefault(); document.body.style.cursor = 'initial'; removeEventListener(); }; window.addEventListener('pointermove', onResize); window.addEventListener('touchmove', onResize); window.addEventListener("touchend", onEventUp); window.addEventListener("pointerup", onEventUp); }; function clamp(value, min, max) { return Math.min(Math.max(value, min), max); } function emitEvent() { sliderElement.dispatchEvent(new CustomEvent('demo.slider.tags', { bubbles: false, detail: { tags: tags, } })); } function getPercent(containerWidth, distanceMoved) { return (distanceMoved / containerWidth) * 100; } function nearestMultiple(divisor, number) { return Math.ceil(number / divisor) * divisor; } // Return public interface of IIFE return self; })(sliderElement); </script>