Last active
October 1, 2025 15:08
-
-
Save alekrutkowski/4249df138da5c2554a734b860bebff06 to your computer and use it in GitHub Desktop.
Double range slider (2 values, e.g. min and max) for Observablehq's "Observable Framework" markdown
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| --- | |
| toc: false | |
| --- | |
| ```js | |
| function DoubleRange({ | |
| min = 0, | |
| max = 100, | |
| step = 1, | |
| value = [min, max], | |
| color = "#3b82f6", | |
| trackColor = "#e5e7eb", | |
| thumbSize = 16 | |
| } = {}) { | |
| let [low, high] = value; | |
| const container = html` | |
| <div style="position:relative; width:100%; padding:2.5em 0 1.5em 0;"> | |
| <!-- Track --> | |
| <div style=" | |
| position:absolute; left:0; right:0; top:50%; height:4px; transform:translateY(-50%); | |
| background:${trackColor}; border-radius:999px;"></div> | |
| <!-- Selected range --> | |
| <div class="range" style=" | |
| position:absolute; height:4px; top:50%; transform:translateY(-50%); | |
| background:${color}; border-radius:999px;"></div> | |
| <!-- Labels --> | |
| <div class="label-low" style="position:absolute; top:8px; transform:translateX(-50%); font:12px system-ui;"></div> | |
| <div class="label-high" style="position:absolute; top:8px; transform:translateX(-50%); font:12px system-ui;"></div> | |
| <!-- Low (min) --> | |
| <input class="low" type="range" min=${min} max=${max} step=${step} | |
| style="position:absolute; left:0; right:0; width:100%; | |
| top:calc(50% - 9px); appearance:none; background:transparent; pointer-events:auto;"> | |
| <!-- High (max) --> | |
| <input class="high" type="range" min=${min} max=${max} step=${step} | |
| style="position:absolute; left:0; right:0; width:100%; | |
| top:calc(50% - 9px); appearance:none; background:transparent; pointer-events:auto;"> | |
| </div> | |
| `; | |
| const inputLow = container.querySelector(".low"); | |
| const inputHigh = container.querySelector(".high"); | |
| const band = container.querySelector(".range"); | |
| const labelLow = container.querySelector(".label-low"); | |
| const labelHigh = container.querySelector(".label-high"); | |
| // Solid circle thumbs | |
| const thumbCSS = html`<style> | |
| input[type=range]::-webkit-slider-thumb { | |
| -webkit-appearance:none; appearance:none; | |
| width:${thumbSize}px; height:${thumbSize}px; border-radius:50%; | |
| background:${color}; cursor:pointer; border:none; box-shadow:none; | |
| } | |
| input[type=range]::-webkit-slider-runnable-track { height:0; background:transparent } | |
| input[type=range]::-moz-range-thumb { | |
| width:${thumbSize}px; height:${thumbSize}px; border-radius:50%; | |
| background:${color}; cursor:pointer; border:none; box-shadow:none; | |
| } | |
| input[type=range]::-moz-range-track { height:0; background:transparent } | |
| input[type=range]::-moz-focus-outer { border:0 } | |
| </style>`; | |
| container.appendChild(thumbCSS); | |
| function sync() { | |
| let lo = +inputLow.value; | |
| let hi = +inputHigh.value; | |
| if (document.activeElement === inputLow && lo > hi) lo = hi; | |
| if (document.activeElement === inputHigh && hi < lo) hi = lo; | |
| inputLow.value = lo; | |
| inputHigh.value = hi; | |
| const percent = v => (100 * (v - min)) / (max - min); | |
| band.style.left = `${percent(lo)}%`; | |
| band.style.right = `${100 - percent(hi)}%`; | |
| // Update labels above each pointer | |
| labelLow.textContent = lo; | |
| labelHigh.textContent = hi; | |
| labelLow.style.left = `${percent(lo)}%`; | |
| labelHigh.style.left = `${percent(hi)}%`; | |
| container.value = [lo, hi]; | |
| container.dispatchEvent(new CustomEvent("input")); | |
| } | |
| inputLow.value = low; | |
| inputHigh.value = high; | |
| sync(); | |
| inputLow.addEventListener("input", sync); | |
| inputHigh.addEventListener("input", sync); | |
| container.name = "double-range"; | |
| return container; | |
| } | |
| ``` | |
| ```js | |
| // Example usage: | |
| const range = view(DoubleRange({min: 0, max: 100, step: 1, value: [20, 80]})) | |
| ``` | |
| <p>${range.join(", ")}</p> |
Author
alekrutkowski
commented
Oct 1, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment