webdevpuneet.com

Dual Range Price Filter — Free HTML CSS JS Snippet

Dual Range Price Filter · Forms · Plain HTML, CSS & JS · Live preview

CategoryForms

Copy & paste

Source Code

<div class="drp-card">
  <div class="drp-head">
    <span class="drp-label">Price range</span>
    <span class="drp-readout" id="drpReadout">$120 – $640</span>
  </div>

  <div class="drp-bars" id="drpBars" aria-hidden="true"></div>

  <div class="drp-slider" id="drpSlider">
    <div class="drp-track"></div>
    <div class="drp-fill" id="drpFill"></div>
    <input class="drp-input drp-min" id="drpMin" type="range" min="0" max="1000" step="10" value="120" aria-label="Minimum price">
    <input class="drp-input drp-max" id="drpMax" type="range" min="0" max="1000" step="10" value="640" aria-label="Maximum price">
  </div>

  <div class="drp-fields">
    <label class="drp-field"><span>Min</span><div class="drp-box"><i>$</i><input id="drpMinNum" type="number" min="0" max="1000" value="120"></div></label>
    <span class="drp-dash"></span>
    <label class="drp-field"><span>Max</span><div class="drp-box"><i>$</i><input id="drpMaxNum" type="number" min="0" max="1000" value="640"></div></label>
  </div>

  <button class="drp-apply" type="button" id="drpApply">Show results</button>
</div>

Extend with AI

Take This Snippet Further With AI

The AI prompt for this one is for Patreon supporters ($3 a month), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.

· Become a supporter

What's included

Features

Two overlaid native ranges
Stacks two real <input type=range> so the dual-thumb control keeps native keyboard, touch, and screen-reader support.
Pointer-events thumb trick
Inputs are click-through except their thumbs, so both handles stay independently draggable while overlapping.
Minimum-gap clamping
clampPair() stops the thumbs from crossing and keeps the range at least one band wide.
Aligned highlight fill
The fill's edges are computed with a per-side correction so it lines up under the thumb centres even at the extremes.
Distribution histogram
Bars show how many items fall in each price band and brighten when inside the selected range, like Airbnb's filter.
Two-way number sync
Dragging updates the number fields and typing a value moves the thumbs, both clamped to bounds and gap.
Clean number inputs
Spinner arrows are hidden and a dollar prefix is shown for a tidy, typeable price entry.
Live readout
The header shows the current min–max range and updates on every change.

About this UI Snippet

Dual Range Price Filter — Two-Thumb Range Slider with Histogram and Synced Inputs

Screenshot of the Dual Range Price Filter snippet rendered live

Filtering by price is one of the most-used controls in any store or marketplace, and the expected interface is a two-thumb range slider: drag the left handle to set the minimum, the right handle for the maximum, with the selected band highlighted between them. This component builds that control properly — overlaid native range inputs for real accessibility, a distribution histogram above the track, synced number inputs below, minimum-gap clamping so the thumbs cannot cross, and a live readout — all in HTML, CSS, and vanilla JavaScript with no slider library.

Two native inputs, one slider

The dual-thumb effect is achieved by stacking two native <input type="range"> elements in the same space, both spanning the full track. Using real range inputs (rather than divs and mouse math) means the control inherits keyboard support, screen-reader semantics, and touch handling for free. The trick that makes them usable together is pointer-events: none on the inputs with pointer-events: auto re-enabled only on the thumbs (::-webkit-slider-thumb and ::-moz-range-thumb). That way clicks on the track pass through to whichever thumb is nearest, and each thumb stays independently draggable even though the two inputs overlap completely.

Preventing the thumbs from crossing

Without a guard, you could drag the minimum past the maximum and invert the range. clampPair() enforces a minimum gap (GAP = 50): when the min thumb moves, it is capped at max - GAP; when the max thumb moves, it is floored at min + GAP. The capped value is written back to the input immediately, so the thumb visibly stops rather than letting the values cross and then snapping back. This keeps the selected range always valid and always at least one band wide.

The highlighted fill

The indigo fill between the thumbs is a positioned element whose left and right are computed from each value as a percentage of the range. A small per-side correction (6 - pct * 0.12 pixels) accounts for the track's 6px horizontal padding and the thumb's radius, so the coloured fill lines up under the centres of the thumbs rather than drifting at the extremes — the detail that separates a polished dual slider from one where the fill is visibly off near the ends.

The distribution histogram

Above the slider sits a small bar histogram generated from a HIST array — a count of how many items fall in each price band. As the range changes, every bar whose centre lies inside the selected range gets an .in class and brightens to the accent tint, while out-of-range bars stay grey. This "price distribution" visual — popularised by Airbnb's price filter — helps users see where most options are priced before they choose, so they do not accidentally filter out the bulk of the inventory.

Two-way sync with number inputs

Below the slider are Min and Max number inputs with dollar prefixes. They stay in sync both ways: dragging a thumb updates the numbers via render(), and typing a number (on change) clamps it to the bounds and the gap, updates the slider thumbs, and re-renders. The number inputs hide their spinner arrows for a clean look and let users type an exact figure when dragging is imprecise. The live readout in the header ($120 – $640) updates on every change so the current selection is always legible.

Customisation

Set MIN, MAX, the step on the inputs, and GAP (the minimum distance between thumbs) to fit your price domain. Replace the HIST array with your real per-band item counts to make the histogram meaningful. Swap the #6366f1 accent used by the thumbs, fill, and histogram, and wire the "Show results" button to your real filtering query using minR.value and maxR.value.

Step by step

How to Use

  1. 1
    Paste the HTML, CSS, and JSA price-range card renders with a histogram, a two-thumb slider, synced Min/Max number inputs, and a live readout.
  2. 2
    Drag a thumbThe fill between the thumbs and the header readout update live, and histogram bars inside the range brighten.
  3. 3
    Try to cross the thumbsThey stop at a fixed minimum gap so the minimum can never pass the maximum and the range stays valid.
  4. 4
    Type an exact valueEnter a number in the Min or Max field; it clamps to the bounds and gap and moves the matching thumb.
  5. 5
    Set your price domainEdit MIN, MAX, step, and GAP, and replace the HIST array with your real per-band item counts.
  6. 6
    Wire up resultsConnect the Show results button to your filter query using the current minR.value and maxR.value.

Real-world uses

Common Use Cases

E-commerce and marketplace filters
Let shoppers narrow products by price with the expected two-thumb control — pair with a faceted filter sidebar and a chip filter for other facets.
Real-estate and rental search
Filter listings by price or rent with a distribution histogram showing where most options sit.
Travel and booking sites
Set a budget band for flights or hotels; complement with a date range picker for dates.
Analytics and data dashboards
Constrain a numeric range (revenue, size, score) to filter a dataset or chart.
Job and salary filters
Let users set a desired salary band when browsing roles.
Learning dual-slider mechanics
A reference for the overlaid-range technique, gap clamping, and two-way slider/number sync; compare with a single-thumb range slider.
Related: GDPR Data Request Form
See the GDPR Data Request Form for a related forms pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Native range inputs give you keyboard control (arrow keys), focus, touch dragging, and screen-reader announcements for free — recreating all of that with divs and pointer events is a lot of code and easy to get wrong. The only trick needed is pointer-events: none on the inputs with pointer-events: auto on their thumbs, so the two overlapping inputs each stay draggable. This keeps the control accessible by default.

clampPair() enforces a minimum gap: when the min input changes it is capped at max - GAP, and when the max input changes it is floored at min + GAP, writing the corrected value straight back to the input so the thumb stops visibly. Adjust the GAP constant for the smallest range you want to allow — set it to the step size if you only need to prevent exact crossing.

Range thumbs are inset from the track edges by roughly their radius, and the track here has 6px of horizontal padding. The render() function applies a small per-side correction (6 - pct * 0.12 px) so the fill's edges track the thumb centres across the whole range instead of drifting near the ends. Tune that factor if your thumb size or track padding differs.

Replace the HIST array with your actual counts of items per price band — one number per bar, in order from low to high price. The bars' heights scale to those counts, and render() marks a bar .in when its band centre falls within the selected range. Use as many entries as you want bars; the band width is computed from (MAX - MIN) / HIST.length.

Keep [min, max] in state. Render two range inputs bound to those values with onChange/onInput handlers that apply the gap clamp before setting state. Compute the fill's left/right and the histogram .in flags from the state during render. The number inputs are controlled inputs that clamp on change. All the CSS — overlaid inputs, thumb styling, fill, histogram — ports unchanged; only the clamp/sync logic moves into your handlers.