Skip to content

Instantly share code, notes, and snippets.

@alekrutkowski
Last active October 1, 2025 15:08
Show Gist options
  • Save alekrutkowski/4249df138da5c2554a734b860bebff06 to your computer and use it in GitHub Desktop.
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
---
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>
@alekrutkowski
Copy link
Author

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment