Open demo

CSS code

HTML
<div><span class="lbl">Price range with min and max boxes</span><div class="card">
<div class="pr" role="group" aria-labelledby="pr-night-title">
  <div class="pr-head"><span class="pr-title" id="pr-night-title">Nightly price</span><output class="pr-sum" for="pr-night-min pr-night-max">$180 – $640</output></div>
  <div class="pr-slider">
    <input type="range" id="pr-night-min" min="0" max="1000" step="10" value="180">
    <input type="range" id="pr-night-max" min="0" max="1000" step="10" value="640">
  </div>
  <div class="pr-boxes">
    <div class="pr-box"><label for="pr-night-min">Minimum</label><output for="pr-night-min">$180</output></div>
    <span class="pr-dash" aria-hidden="true">–</span>
    <div class="pr-box"><label for="pr-night-max">Maximum</label><output for="pr-night-max">$640</output></div>
  </div>
</div></div></div>

<div><span class="lbl">With a price histogram</span><div class="card">
<div class="pr solid" role="group" aria-labelledby="pr-home-title">
  <div class="pr-head"><span class="pr-title" id="pr-home-title">Home price</span><output class="pr-sum" for="pr-home-min pr-home-max">$350,000 – $900,000</output></div>
  <div class="pr-bars" aria-hidden="true">
    <span style="height:8%"></span><span style="height:14%"></span><span style="height:22%"></span><span style="height:38%"></span>
    <span style="height:55%"></span><span style="height:72%"></span><span style="height:88%"></span><span style="height:100%"></span>
    <span style="height:94%"></span><span style="height:81%"></span><span style="height:70%"></span><span style="height:62%"></span>
    <span style="height:51%"></span><span style="height:44%"></span><span style="height:36%"></span><span style="height:30%"></span>
    <span style="height:24%"></span><span style="height:19%"></span><span style="height:15%"></span><span style="height:12%"></span>
    <span style="height:9%"></span><span style="height:7%"></span><span style="height:5%"></span><span style="height:4%"></span>
  </div>
  <div class="pr-slider">
    <input type="range" id="pr-home-min" min="100000" max="2000000" step="25000" value="350000" aria-label="Minimum home price">
    <input type="range" id="pr-home-max" min="100000" max="2000000" step="25000" value="900000" aria-label="Maximum home price">
  </div>
</div></div></div>

<div><span class="lbl">Time window</span><div class="card">
<div class="pr time" role="group" aria-labelledby="pr-dep-title" data-format="time">
  <div class="pr-head"><span class="pr-title" id="pr-dep-title">Departure time</span><output class="pr-sum" for="pr-dep-from pr-dep-to">06:30 – 18:00</output></div>
  <div class="pr-slider">
    <input type="range" id="pr-dep-from" min="0" max="1440" step="30" value="390" aria-label="Earliest departure">
    <input type="range" id="pr-dep-to" min="0" max="1440" step="30" value="1080" aria-label="Latest departure">
  </div>
  <div class="pr-scale" aria-hidden="true"><span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>24:00</span></div>
</div></div></div>
CSS
/* Two-thumb range sliders: two real <input type="range"> stacked on one track. The inputs ignore the pointer and
   only their thumbs take it, so either thumb can be dragged. The script keeps min <= max and sets
   --lo and --hi (0 to 1), which paint the selected stretch of the track. */
.pr{--pr-accent:#e11d48;--pr-track:#8e98a8;--pr-thumb:22px;--pr-h:6px;--lo:0;--hi:1;display:grid;gap:14px}
.pr-head{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap;gap:4px 12px}
.pr-title{font-size:.95rem;font-weight:600;color:#1e293b}
.pr-sum{font-size:.95rem;font-weight:700;color:#be123c;font-variant-numeric:tabular-nums}
.pr-slider{position:relative;height:var(--pr-thumb)}
.pr-slider::before{content:"";position:absolute;left:0;right:0;top:50%;height:var(--pr-h);transform:translateY(-50%);border-radius:999px;
  background:linear-gradient(to right,
    var(--pr-track) 0 calc(var(--pr-thumb) / 2 + (100% - var(--pr-thumb)) * var(--lo)),
    var(--pr-accent) 0 calc(var(--pr-thumb) / 2 + (100% - var(--pr-thumb)) * var(--hi)),
    var(--pr-track) 0)}
.pr-slider input[type="range"]{-webkit-appearance:none;appearance:none;position:absolute;inset:0;width:100%;height:100%;
  margin:0;background:none;pointer-events:none}
.pr-slider input[type="range"]:focus-visible{outline:none}
.pr-slider input[type="range"]::-webkit-slider-runnable-track{height:var(--pr-h);background:none}
.pr-slider input[type="range"]::-moz-range-track{height:var(--pr-h);background:none}
.pr-slider input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;box-sizing:border-box;pointer-events:auto;cursor:grab;
  width:var(--pr-thumb);height:var(--pr-thumb);margin-top:calc((var(--pr-h) - var(--pr-thumb)) / 2);
  border:2px solid var(--pr-accent);border-radius:50%;background:#fff;box-shadow:0 1px 4px rgba(15,23,42,.3)}
.pr-slider input[type="range"]::-moz-range-thumb{box-sizing:border-box;pointer-events:auto;cursor:grab;
  width:var(--pr-thumb);height:var(--pr-thumb);border:2px solid var(--pr-accent);border-radius:50%;background:#fff;box-shadow:0 1px 4px rgba(15,23,42,.3)}
.pr-slider input[type="range"]:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--pr-accent)}
.pr-slider input[type="range"]:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--pr-accent)}
/* min / max boxes under the track: visible <label>s for the two inputs */
.pr-boxes{display:flex;align-items:center;gap:10px;margin-top:4px}
.pr-box{flex:1;display:grid;gap:2px;padding:8px 12px;border:1px solid #8e98a8;border-radius:12px}
.pr-box label{font-size:.75rem;color:#5d6472}
.pr-box output{font-size:1rem;font-weight:700;color:#1e293b;font-variant-numeric:tabular-nums}
.pr-dash{color:#5d6472}
/* histogram: bars inside the selected range get the accent (the script sets data-in) */
.pr-bars{display:flex;align-items:flex-end;gap:3px;height:64px;margin:6px calc(var(--pr-thumb) / 2) -12px}
.pr-bars span{flex:1;border-radius:3px 3px 0 0;background:#d9dde4}
.pr-bars span[data-in]{background:#fb7185}
.pr.solid .pr-slider input[type="range"]::-webkit-slider-thumb{border:3px solid #fff;background:var(--pr-accent)}
.pr.solid .pr-slider input[type="range"]::-moz-range-thumb{border:3px solid #fff;background:var(--pr-accent)}
/* time window: pill thumbs and an hour scale */
.pr.time{--pr-thumb:16px;--pr-h:4px}
.pr.time .pr-slider{height:26px}
.pr.time .pr-slider input[type="range"]::-webkit-slider-thumb{height:26px;margin-top:-11px;border-radius:8px;border:0;background:var(--pr-accent)}
.pr.time .pr-slider input[type="range"]::-moz-range-thumb{height:26px;border-radius:8px;border:0;background:var(--pr-accent)}
.pr-scale{display:flex;justify-content:space-between;margin:-4px calc(var(--pr-thumb) / 2) 0;font-size:.78rem;color:#5d6472;font-variant-numeric:tabular-nums}
.pr-scale span{width:0;display:flex;justify-content:center;white-space:nowrap}
JavaScript
// Two-thumb sliders: keeps min <= max, paints the selected range (--lo, --hi) and updates the readouts.
document.querySelectorAll('.pr').forEach(function (group) {
  var ranges = group.querySelectorAll('input[type="range"]'), lo = ranges[0], hi = ranges[1];
  var min = +lo.min, max = +lo.max, bars = group.querySelectorAll('.pr-bars span');
  function fmt(v) {
    v = +v;
    if (group.dataset.format === 'time') return String(Math.floor(v / 60)).padStart(2, '0') + ':' + String(v % 60).padStart(2, '0');
    return '$' + v.toLocaleString('en-US');
  }
  function sync(e) {
    if (+lo.value > +hi.value) { if (e && e.target === lo) lo.value = hi.value; else hi.value = lo.value; }
    var a = (lo.value - min) / (max - min), b = (hi.value - min) / (max - min);
    group.style.setProperty('--lo', a);
    group.style.setProperty('--hi', b);
    lo.style.zIndex = (a === b && a > 0.5) ? 2 : ''; // when the thumbs meet on the right, keep the min thumb on top
    [lo, hi].forEach(function (r) {
      var out = group.querySelector('output[for="' + r.id + '"]');
      r.setAttribute('aria-valuetext', fmt(r.value));
      if (out) out.textContent = fmt(r.value);
    });
    var sum = group.querySelector('.pr-sum');
    if (sum) sum.textContent = fmt(lo.value) + ' – ' + fmt(hi.value);
    bars.forEach(function (bar, i) { var c = (i + 0.5) / bars.length; bar.toggleAttribute('data-in', c >= a && c <= b); });
  }
  lo.addEventListener('input', sync);
  hi.addEventListener('input', sync);
  sync();
});

Bootstrap 5 code

Requires: Bootstrap 5.3.8 CSS

HTML
<div><span class="lbl">Price range with min and max boxes</span><div class="card demo-card"><div class="card-body p-4">
<div class="pr" role="group" aria-labelledby="pr-night-title">
  <div class="d-flex justify-content-between align-items-baseline flex-wrap column-gap-3 row-gap-1"><span class="pr-title fw-semibold" id="pr-night-title">Nightly price</span><output class="pr-sum fw-bold" for="pr-night-min pr-night-max">$180 – $640</output></div>
  <div class="pr-slider">
    <input type="range" class="form-range" id="pr-night-min" min="0" max="1000" step="10" value="180">
    <input type="range" class="form-range" id="pr-night-max" min="0" max="1000" step="10" value="640">
  </div>
  <div class="pr-boxes d-flex align-items-center gap-2">
    <div class="pr-box flex-fill d-grid px-3 py-2"><label class="form-label mb-0" for="pr-night-min">Minimum</label><output for="pr-night-min">$180</output></div>
    <span class="pr-dash" aria-hidden="true">–</span>
    <div class="pr-box flex-fill d-grid px-3 py-2"><label class="form-label mb-0" for="pr-night-max">Maximum</label><output for="pr-night-max">$640</output></div>
  </div>
</div></div></div></div>

<div><span class="lbl">With a price histogram</span><div class="card demo-card"><div class="card-body p-4">
<div class="pr solid" role="group" aria-labelledby="pr-home-title">
  <div class="d-flex justify-content-between align-items-baseline flex-wrap column-gap-3 row-gap-1"><span class="pr-title fw-semibold" id="pr-home-title">Home price</span><output class="pr-sum fw-bold" for="pr-home-min pr-home-max">$350,000 – $900,000</output></div>
  <div class="pr-bars" aria-hidden="true">
    <span style="height:8%"></span><span style="height:14%"></span><span style="height:22%"></span><span style="height:38%"></span>
    <span style="height:55%"></span><span style="height:72%"></span><span style="height:88%"></span><span style="height:100%"></span>
    <span style="height:94%"></span><span style="height:81%"></span><span style="height:70%"></span><span style="height:62%"></span>
    <span style="height:51%"></span><span style="height:44%"></span><span style="height:36%"></span><span style="height:30%"></span>
    <span style="height:24%"></span><span style="height:19%"></span><span style="height:15%"></span><span style="height:12%"></span>
    <span style="height:9%"></span><span style="height:7%"></span><span style="height:5%"></span><span style="height:4%"></span>
  </div>
  <div class="pr-slider">
    <input type="range" class="form-range" id="pr-home-min" min="100000" max="2000000" step="25000" value="350000" aria-label="Minimum home price">
    <input type="range" class="form-range" id="pr-home-max" min="100000" max="2000000" step="25000" value="900000" aria-label="Maximum home price">
  </div>
</div></div></div></div>

<div><span class="lbl">Time window</span><div class="card demo-card"><div class="card-body p-4">
<div class="pr time" role="group" aria-labelledby="pr-dep-title" data-format="time">
  <div class="d-flex justify-content-between align-items-baseline flex-wrap column-gap-3 row-gap-1"><span class="pr-title fw-semibold" id="pr-dep-title">Departure time</span><output class="pr-sum fw-bold" for="pr-dep-from pr-dep-to">06:30 – 18:00</output></div>
  <div class="pr-slider">
    <input type="range" class="form-range" id="pr-dep-from" min="0" max="1440" step="30" value="390" aria-label="Earliest departure">
    <input type="range" class="form-range" id="pr-dep-to" min="0" max="1440" step="30" value="1080" aria-label="Latest departure">
  </div>
  <div class="pr-scale d-flex justify-content-between" aria-hidden="true"><span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>24:00</span></div>
</div></div></div></div>
CSS
/* Two-thumb range sliders: two Bootstrap .form-range inputs stacked on one track. The inputs ignore the pointer and
   only their thumbs take it, so either thumb can be dragged. The script keeps min <= max and sets
   --lo and --hi (0 to 1), which paint the selected stretch of the track. */
.pr{--pr-accent:#e11d48;--pr-track:#8e98a8;--pr-thumb:22px;--pr-h:6px;--lo:0;--hi:1;display:grid;gap:14px}
.pr-title{font-size:.95rem;color:#1e293b}
.pr-sum{font-size:.95rem;color:#be123c;font-variant-numeric:tabular-nums}
.pr-slider{position:relative;height:var(--pr-thumb)}
.pr-slider::before{content:"";position:absolute;left:0;right:0;top:50%;height:var(--pr-h);transform:translateY(-50%);border-radius:999px;
  background:linear-gradient(to right,
    var(--pr-track) 0 calc(var(--pr-thumb) / 2 + (100% - var(--pr-thumb)) * var(--lo)),
    var(--pr-accent) 0 calc(var(--pr-thumb) / 2 + (100% - var(--pr-thumb)) * var(--hi)),
    var(--pr-track) 0)}
.pr-slider .form-range{position:absolute;inset:0;height:100%;pointer-events:none}
.pr-slider .form-range::-webkit-slider-runnable-track{height:var(--pr-h);background:none}
.pr-slider .form-range::-moz-range-track{height:var(--pr-h);background:none}
.pr-slider .form-range::-webkit-slider-thumb{box-sizing:border-box;pointer-events:auto;cursor:grab;
  width:var(--pr-thumb);height:var(--pr-thumb);margin-top:calc((var(--pr-h) - var(--pr-thumb)) / 2);
  border:2px solid var(--pr-accent);border-radius:50%;background:#fff;box-shadow:0 1px 4px rgba(15,23,42,.3)}
.pr-slider .form-range::-moz-range-thumb{box-sizing:border-box;pointer-events:auto;cursor:grab;
  width:var(--pr-thumb);height:var(--pr-thumb);border:2px solid var(--pr-accent);border-radius:50%;background:#fff;box-shadow:0 1px 4px rgba(15,23,42,.3)}
.pr-slider .form-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--pr-accent)}
.pr-slider .form-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 3px #fff,0 0 0 5px var(--pr-accent)}
/* min / max boxes under the track: visible <label>s for the two inputs */
.pr-boxes{margin-top:4px}
.pr-box{gap:2px;border:1px solid #8e98a8;border-radius:12px}
.pr-box .form-label{font-size:.75rem;color:#5d6472}
.pr-box output{font-size:1rem;font-weight:700;color:#1e293b;font-variant-numeric:tabular-nums}
.pr-dash{color:#5d6472}
/* histogram: bars inside the selected range get the accent (the script sets data-in) */
.pr-bars{display:flex;align-items:flex-end;gap:3px;height:64px;margin:6px calc(var(--pr-thumb) / 2) -12px}
.pr-bars span{flex:1;border-radius:3px 3px 0 0;background:#d9dde4}
.pr-bars span[data-in]{background:#fb7185}
.pr.solid .pr-slider .form-range::-webkit-slider-thumb{border:3px solid #fff;background:var(--pr-accent)}
.pr.solid .pr-slider .form-range::-moz-range-thumb{border:3px solid #fff;background:var(--pr-accent)}
/* time window: pill thumbs and an hour scale */
.pr.time{--pr-thumb:16px;--pr-h:4px}
.pr.time .pr-slider{height:26px}
.pr.time .pr-slider .form-range::-webkit-slider-thumb{height:26px;margin-top:-11px;border-radius:8px;border:0;background:var(--pr-accent)}
.pr.time .pr-slider .form-range::-moz-range-thumb{height:26px;border-radius:8px;border:0;background:var(--pr-accent)}
.pr-scale{margin:-4px calc(var(--pr-thumb) / 2) 0;font-size:.78rem;color:#5d6472;font-variant-numeric:tabular-nums}
.pr-scale span{width:0;display:flex;justify-content:center;white-space:nowrap}
JavaScript
// Two-thumb sliders: keeps min <= max, paints the selected range (--lo, --hi) and updates the readouts.
document.querySelectorAll('.pr').forEach(function (group) {
  var ranges = group.querySelectorAll('.form-range'), lo = ranges[0], hi = ranges[1];
  var min = +lo.min, max = +lo.max, bars = group.querySelectorAll('.pr-bars span');
  function fmt(v) {
    v = +v;
    if (group.dataset.format === 'time') return String(Math.floor(v / 60)).padStart(2, '0') + ':' + String(v % 60).padStart(2, '0');
    return '$' + v.toLocaleString('en-US');
  }
  function sync(e) {
    if (+lo.value > +hi.value) { if (e && e.target === lo) lo.value = hi.value; else hi.value = lo.value; }
    var a = (lo.value - min) / (max - min), b = (hi.value - min) / (max - min);
    group.style.setProperty('--lo', a);
    group.style.setProperty('--hi', b);
    lo.style.zIndex = (a === b && a > 0.5) ? 2 : ''; // when the thumbs meet on the right, keep the min thumb on top
    [lo, hi].forEach(function (r) {
      var out = group.querySelector('output[for="' + r.id + '"]');
      r.setAttribute('aria-valuetext', fmt(r.value));
      if (out) out.textContent = fmt(r.value);
    });
    var sum = group.querySelector('.pr-sum');
    if (sum) sum.textContent = fmt(lo.value) + ' – ' + fmt(hi.value);
    bars.forEach(function (bar, i) { var c = (i + 0.5) / bars.length; bar.toggleAttribute('data-in', c >= a && c <= b); });
  }
  lo.addEventListener('input', sync);
  hi.addEventListener('input', sync);
  sync();
});

Tailwind 4 code

Requires: Tailwind CSS 4

HTML
<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5d6472] uppercase">Price range with min and max boxes</span><div class="rounded-2xl border border-[#ece3e4] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(15,23,42,.04)]">
<div class="pr grid gap-3.5 [--hi:1] [--lo:0]" role="group" aria-labelledby="pr-night-title">
  <div class="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1"><span id="pr-night-title" class="text-[.95rem] font-semibold">Nightly price</span><output for="pr-night-min pr-night-max" class="pr-sum text-[.95rem] font-bold text-pr-strong tabular-nums">$180 – $640</output></div>
  <div class="relative h-[22px] before:absolute before:inset-x-0 before:top-1/2 before:h-1.5 before:-translate-y-1/2 before:rounded-full before:bg-[linear-gradient(to_right,var(--color-pr-track)_0_calc(11px_+_(100%_-_22px)_*_var(--lo)),var(--color-pr-accent)_0_calc(11px_+_(100%_-_22px)_*_var(--hi)),var(--color-pr-track)_0)]">
    <input type="range" id="pr-night-min" min="0" max="1000" step="10" value="180" class="pointer-events-none absolute inset-0 m-0 size-full appearance-none bg-transparent focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:bg-transparent [&::-moz-range-track]:h-1.5 [&::-moz-range-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:-mt-2 [&::-webkit-slider-thumb]:size-[22px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-pr-accent [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.3)] [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-[22px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-pr-accent [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.3)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-pr-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-pr-accent)]">
    <input type="range" id="pr-night-max" min="0" max="1000" step="10" value="640" class="pointer-events-none absolute inset-0 m-0 size-full appearance-none bg-transparent focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:bg-transparent [&::-moz-range-track]:h-1.5 [&::-moz-range-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:-mt-2 [&::-webkit-slider-thumb]:size-[22px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-pr-accent [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.3)] [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-[22px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-pr-accent [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.3)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-pr-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-pr-accent)]">
  </div>
  <div class="mt-1 flex items-center gap-2.5">
    <div class="grid flex-1 gap-0.5 rounded-xl border border-pr-track px-3 py-2"><label for="pr-night-min" class="text-xs text-[#5d6472]">Minimum</label><output for="pr-night-min" class="font-bold tabular-nums">$180</output></div>
    <span class="text-[#5d6472]" aria-hidden="true">–</span>
    <div class="grid flex-1 gap-0.5 rounded-xl border border-pr-track px-3 py-2"><label for="pr-night-max" class="text-xs text-[#5d6472]">Maximum</label><output for="pr-night-max" class="font-bold tabular-nums">$640</output></div>
  </div>
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5d6472] uppercase">With a price histogram</span><div class="rounded-2xl border border-[#ece3e4] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(15,23,42,.04)]">
<div class="pr grid gap-3.5 [--hi:1] [--lo:0]" role="group" aria-labelledby="pr-home-title">
  <div class="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1"><span id="pr-home-title" class="text-[.95rem] font-semibold">Home price</span><output for="pr-home-min pr-home-max" class="pr-sum text-[.95rem] font-bold text-pr-strong tabular-nums">$350,000 – $900,000</output></div>
  <div class="pr-bars mx-[11px] mt-1.5 -mb-3 flex h-16 items-end gap-[3px] *:flex-1 *:rounded-t-[3px] *:bg-[#d9dde4] *:data-in:bg-[#fb7185]" aria-hidden="true">
    <span style="height:8%"></span><span style="height:14%"></span><span style="height:22%"></span><span style="height:38%"></span>
    <span style="height:55%"></span><span style="height:72%"></span><span style="height:88%"></span><span style="height:100%"></span>
    <span style="height:94%"></span><span style="height:81%"></span><span style="height:70%"></span><span style="height:62%"></span>
    <span style="height:51%"></span><span style="height:44%"></span><span style="height:36%"></span><span style="height:30%"></span>
    <span style="height:24%"></span><span style="height:19%"></span><span style="height:15%"></span><span style="height:12%"></span>
    <span style="height:9%"></span><span style="height:7%"></span><span style="height:5%"></span><span style="height:4%"></span>
  </div>
  <div class="relative h-[22px] before:absolute before:inset-x-0 before:top-1/2 before:h-1.5 before:-translate-y-1/2 before:rounded-full before:bg-[linear-gradient(to_right,var(--color-pr-track)_0_calc(11px_+_(100%_-_22px)_*_var(--lo)),var(--color-pr-accent)_0_calc(11px_+_(100%_-_22px)_*_var(--hi)),var(--color-pr-track)_0)]">
    <input type="range" id="pr-home-min" min="100000" max="2000000" step="25000" value="350000" aria-label="Minimum home price" class="pointer-events-none absolute inset-0 m-0 size-full appearance-none bg-transparent focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:bg-transparent [&::-moz-range-track]:h-1.5 [&::-moz-range-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:-mt-2 [&::-webkit-slider-thumb]:size-[22px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-3 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-pr-accent [&::-webkit-slider-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.3)] [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-[22px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-3 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-pr-accent [&::-moz-range-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.3)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-pr-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-pr-accent)]">
    <input type="range" id="pr-home-max" min="100000" max="2000000" step="25000" value="900000" aria-label="Maximum home price" class="pointer-events-none absolute inset-0 m-0 size-full appearance-none bg-transparent focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:bg-transparent [&::-moz-range-track]:h-1.5 [&::-moz-range-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:-mt-2 [&::-webkit-slider-thumb]:size-[22px] [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-3 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-pr-accent [&::-webkit-slider-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.3)] [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-[22px] [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-3 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-pr-accent [&::-moz-range-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.3)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-pr-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-pr-accent)]">
  </div>
</div></div></div>

<div><span class="mb-3 block text-[.72rem] tracking-[.12em] text-[#5d6472] uppercase">Time window</span><div class="rounded-2xl border border-[#ece3e4] bg-white px-6 pt-[22px] pb-6 shadow-[0_1px_2px_rgba(15,23,42,.04)]">
<div class="pr grid gap-3.5 [--hi:1] [--lo:0]" role="group" aria-labelledby="pr-dep-title" data-format="time">
  <div class="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1"><span id="pr-dep-title" class="text-[.95rem] font-semibold">Departure time</span><output for="pr-dep-from pr-dep-to" class="pr-sum text-[.95rem] font-bold text-pr-strong tabular-nums">06:30 – 18:00</output></div>
  <div class="relative h-[26px] before:absolute before:inset-x-0 before:top-1/2 before:h-1 before:-translate-y-1/2 before:rounded-full before:bg-[linear-gradient(to_right,var(--color-pr-track)_0_calc(8px_+_(100%_-_16px)_*_var(--lo)),var(--color-pr-accent)_0_calc(8px_+_(100%_-_16px)_*_var(--hi)),var(--color-pr-track)_0)]">
    <input type="range" id="pr-dep-from" min="0" max="1440" step="30" value="390" aria-label="Earliest departure" class="pointer-events-none absolute inset-0 m-0 size-full appearance-none bg-transparent focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-1 [&::-webkit-slider-runnable-track]:bg-transparent [&::-moz-range-track]:h-1 [&::-moz-range-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:-mt-[11px] [&::-webkit-slider-thumb]:h-[26px] [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-lg [&::-webkit-slider-thumb]:border-0 [&::-webkit-slider-thumb]:bg-pr-accent [&::-webkit-slider-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.3)] [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:h-[26px] [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-lg [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-pr-accent [&::-moz-range-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.3)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-pr-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-pr-accent)]">
    <input type="range" id="pr-dep-to" min="0" max="1440" step="30" value="1080" aria-label="Latest departure" class="pointer-events-none absolute inset-0 m-0 size-full appearance-none bg-transparent focus-visible:outline-none [&::-webkit-slider-runnable-track]:h-1 [&::-webkit-slider-runnable-track]:bg-transparent [&::-moz-range-track]:h-1 [&::-moz-range-track]:bg-transparent [&::-webkit-slider-thumb]:pointer-events-auto [&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:-mt-[11px] [&::-webkit-slider-thumb]:h-[26px] [&::-webkit-slider-thumb]:w-4 [&::-webkit-slider-thumb]:rounded-lg [&::-webkit-slider-thumb]:border-0 [&::-webkit-slider-thumb]:bg-pr-accent [&::-webkit-slider-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.3)] [&::-moz-range-thumb]:pointer-events-auto [&::-moz-range-thumb]:cursor-grab [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:h-[26px] [&::-moz-range-thumb]:w-4 [&::-moz-range-thumb]:rounded-lg [&::-moz-range-thumb]:border-0 [&::-moz-range-thumb]:bg-pr-accent [&::-moz-range-thumb]:shadow-[0_1px_4px_rgba(15,23,42,.3)] focus-visible:[&::-webkit-slider-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-pr-accent)] focus-visible:[&::-moz-range-thumb]:shadow-[0_0_0_3px_#fff,0_0_0_5px_var(--color-pr-accent)]">
  </div>
  <div class="mx-2 -mt-1 flex justify-between text-[.78rem] text-[#5d6472] tabular-nums *:flex *:w-0 *:justify-center *:whitespace-nowrap" aria-hidden="true"><span>00:00</span><span>06:00</span><span>12:00</span><span>18:00</span><span>24:00</span></div>
</div></div></div>
CSS
@theme {
  --color-pr-accent: #e11d48;
  --color-pr-strong: #be123c;
  --color-pr-track: #8e98a8;
}
JavaScript
// Two-thumb sliders: keeps min <= max, paints the selected range (--lo, --hi) and updates the readouts.
document.querySelectorAll('.pr').forEach(function (group) {
  var ranges = group.querySelectorAll('input[type="range"]'), lo = ranges[0], hi = ranges[1];
  var min = +lo.min, max = +lo.max, bars = group.querySelectorAll('.pr-bars span');
  function fmt(v) {
    v = +v;
    if (group.dataset.format === 'time') return String(Math.floor(v / 60)).padStart(2, '0') + ':' + String(v % 60).padStart(2, '0');
    return '$' + v.toLocaleString('en-US');
  }
  function sync(e) {
    if (+lo.value > +hi.value) { if (e && e.target === lo) lo.value = hi.value; else hi.value = lo.value; }
    var a = (lo.value - min) / (max - min), b = (hi.value - min) / (max - min);
    group.style.setProperty('--lo', a);
    group.style.setProperty('--hi', b);
    lo.style.zIndex = (a === b && a > 0.5) ? 2 : ''; // when the thumbs meet on the right, keep the min thumb on top
    [lo, hi].forEach(function (r) {
      var out = group.querySelector('output[for="' + r.id + '"]');
      r.setAttribute('aria-valuetext', fmt(r.value));
      if (out) out.textContent = fmt(r.value);
    });
    var sum = group.querySelector('.pr-sum');
    if (sum) sum.textContent = fmt(lo.value) + ' – ' + fmt(hi.value);
    bars.forEach(function (bar, i) { var c = (i + 0.5) / bars.length; bar.toggleAttribute('data-in', c >= a && c <= b); });
  }
  lo.addEventListener('input', sync);
  hi.addEventListener('input', sync);
  sync();
});

About this set

Two-thumb range sliders for filters. Price range with min and max boxes is the shop and booking pattern: a nightly price from $0 to $1,000 with the selected span in the header and a labelled Minimum and Maximum box under the track. With a price histogram adds 24 bars above a home price slider, and the bars inside the chosen range turn from grey to rose so buyers see how many listings they are keeping. Time window swaps prices for a departure window in 30 minute steps with pill thumbs and an hour scale. Each one is two real range inputs stacked on one track: the inputs ignore the pointer and only their thumbs take it, so either thumb can be dragged, clicked into and moved with the keyboard on its own. About twenty lines of JavaScript keep the minimum from passing the maximum, lift the minimum thumb above the other when they meet on the right so it can still be dragged, paint the stretch between them through –lo and –hi, and write readable values such as 06:30 into the outputs and aria-valuetext.

What’s included

  • Two real range inputs sharing one track
  • Script keeps min at or below max and handles thumbs that meet
  • Min and max boxes with visible labels for each input
  • Histogram whose bars light up inside the selected range
  • Time window formatted as hours and minutes
  • Group labelled with role="group" and aria-labelledby

Accessibility

Each pair sits in a role="group" named by its visible heading. The two inputs are named Minimum and Maximum by visible labels or by aria-label, and each announces its own formatted value, such as $180 or 06:30, through aria-valuetext. Both thumbs are in the tab order, respond to the arrow keys and show a ring when focused.

Customise it

Change --pr-accent, --pr-track, --pr-thumb and --pr-h on .pr (plain CSS and Bootstrap) or the --color-pr-* theme colours in Tailwind. The histogram heights are inline percentages on the bars, and data-format="time" on the group switches the readout to hours and minutes.