Components / Form & Input / Dual Range
Dual Range beta
Two thumbs on one track: the price-range slider every filter sidebar actually needs.
<div class="k-dr" data-dr data-dr-gap="50">
<div class="k-dr-track">
<span class="k-dr-fill"></span>
<input type="range" min="0" max="1000" step="10" value="150" data-dr-lo aria-label="Minimum price">
<input type="range" min="0" max="1000" step="10" value="720" data-dr-hi aria-label="Maximum price">
</div>
<div class="k-dr-out"><span>From <output data-dr-out-lo>$150</output></span><span>To <output data-dr-out-hi>$720</output></span></div>
</div>/* KATA Dual Range — two thumbs, one honest track. */
.k-dr{font-family:var(--k-font);color:var(--k-ink);width:100%;max-width:340px}
.k-dr-track{position:relative;height:26px}
.k-dr-track::before{content:"";position:absolute;left:0;right:0;top:11px;height:4px;border-radius:3px;background:var(--k-zinc-200)}
.k-dr-fill{position:absolute;top:11px;height:4px;border-radius:3px;background:var(--k-accent)}
.k-dr input[type=range]{position:absolute;left:0;top:0;width:100%;height:26px;margin:0;background:none;-webkit-appearance:none;appearance:none;pointer-events:none}
.k-dr input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;width:19px;height:19px;border-radius:50%;background:var(--k-bg);border:2px solid var(--k-zinc-400);box-shadow:0 1px 4px rgba(16,24,40,.18);pointer-events:auto;cursor:grab}
.k-dr input[type=range]::-moz-range-thumb{width:17px;height:17px;border-radius:50%;background:var(--k-bg);border:2px solid var(--k-zinc-400);box-shadow:0 1px 4px rgba(16,24,40,.18);pointer-events:auto;cursor:grab}
.k-dr input[type=range]:focus-visible{outline:2px solid var(--k-accent);outline-offset:4px;border-radius:6px}
.k-dr-out{display:flex;justify-content:space-between;font-size:13px;margin-top:7px}
.k-dr-out output{font-weight:650;font-variant-numeric:tabular-nums}
.k-dr-out span{color:var(--k-ink-3)}
// KATA Dual Range — keeps the gap, paints the fill, mirrors the numbers.
document.querySelectorAll('[data-dr]').forEach(function (root) {
var lo = root.querySelector('[data-dr-lo]');
var hi = root.querySelector('[data-dr-hi]');
var fill = root.querySelector('.k-dr-fill');
var outLo = root.querySelector('[data-dr-out-lo]');
var outHi = root.querySelector('[data-dr-out-hi]');
var GAP = Number(root.getAttribute('data-dr-gap') || 50);
var min = Number(lo.min), max = Number(lo.max);
var fmt = function (v) { return '$' + Number(v).toLocaleString('en-US'); };
function paint() {
var a = Number(lo.value), b = Number(hi.value);
var pa = (a - min) / (max - min) * 100, pb = (b - min) / (max - min) * 100;
fill.style.left = pa + '%';
fill.style.width = (pb - pa) + '%';
outLo.textContent = fmt(a);
outHi.textContent = fmt(b);
lo.setAttribute('aria-valuetext', fmt(a));
hi.setAttribute('aria-valuetext', fmt(b));
}
lo.addEventListener('input', function () { if (Number(lo.value) > Number(hi.value) - GAP) lo.value = Number(hi.value) - GAP; paint(); });
hi.addEventListener('input', function () { if (Number(hi.value) < Number(lo.value) + GAP) hi.value = Number(lo.value) + GAP; paint(); });
paint();
});
Installation
CLI
node kata-ui.mjs add dual-range
Writes the part’s CSS and JS into your project. New to the CLI? See the CLI doc.
Manual
- Load
tokens.cssonce, before any part. - Copy the CSS tab into your stylesheet.
- Copy the HTML where it belongs. The part has a script — include it after the markup; it initializes itself.
Usage
Two native range inputs share one track; the fill between the thumbs is the selection, and the script keeps a minimum gap so the thumbs cannot cross into nonsense. Values mirror into the outputs beside it — wire those to your filter, and the URL if you are serious. Steps, min and max are plain attributes on the inputs.
Accessibility
Both thumbs are real range inputs with their own labels ("Minimum price", "Maximum price"), fully keyboard-operable with arrow keys — the overlap is paint, not a custom widget. The visible numbers are outputs tied to the inputs, so what is announced is what is shown.
Anatomy
The public surface of this part, read from its own source. Class names are the API — the CSS tab above is the full contract.
.k-dr | class |
.k-dr-track | class |
.k-dr-fill | class |
.k-dr-out | class |
data-dr | data hook |
data-dr-gap | data hook |
data-dr-lo | data hook |
data-dr-hi | data hook |
data-dr-out-lo | data hook |
data-dr-out-hi | data hook |
aria-label | ARIA |
Related parts
Button stable
Black leads; lines and ghosts support. Hover, disabled and loading states included.
Button Group stable
Buttons joined into one piece: shared edges, one radius, no gaps. For toolbars, paging and split actions.
Calendar stable
One month, standing on its own: a grid of real day buttons with today marked and a selection you can move.
Cascader beta
A select that walks a tree: pick a branch, then a branch of the branch, until the leaf is the answer.