Components / Form & Input / Dual Range

Dual Range beta

Two thumbs on one track: the price-range slider every filter sidebar actually needs.

Preview
From $150To $720
<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

  1. Load tokens.css once, before any part.
  2. Copy the CSS tab into your stylesheet.
  3. 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-drclass
.k-dr-trackclass
.k-dr-fillclass
.k-dr-outclass
data-drdata hook
data-dr-gapdata hook
data-dr-lodata hook
data-dr-hidata hook
data-dr-out-lodata hook
data-dr-out-hidata hook
aria-labelARIA

Related parts