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>

Dual Range Price Filter — Free HTML CSS JS Snippet

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

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.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than puzzling out the overlapping inputs yourself, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how pointer-events: none on the range inputs combined with pointer-events: auto only on their thumb pseudo-elements lets two fully overlapping sliders both stay independently draggable, and why clampPair() writes the corrected value back to the input immediately instead of just adjusting the rendered fill. The same assistant can help optimize it, for instance checking whether the per-side correction formula in render() (6 minus pct times 0.12) generalizes correctly if the track padding or thumb size changes. It's also useful for extending the filter: ask it to add currency formatting for non-dollar locales, make the histogram clickable to jump the nearest thumb to that band, or persist the selected range to the URL query string so filtered results are shareable. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a dual-thumb price range filter in plain HTML, CSS, and JavaScript using two overlapping native range inputs — no slider library.

Requirements:
- Two native input type=range elements stacked in exactly the same position, both spanning the full width of the track, with pointer-events disabled on the inputs themselves but re-enabled specifically on their thumb (the browser-specific slider-thumb pseudo-element) so clicks pass through to whichever thumb is nearest while each thumb remains independently draggable.
- A visual fill element positioned between the two thumbs whose left and right edges are computed as percentages of the current min and max values relative to the slider's overall min/max range, with a small pixel correction so the fill's edges align under the thumb centers rather than drifting at the extreme ends.
- A clamping function that enforces a minimum gap between the two values: moving the low thumb past (high value minus the gap) must cap it there, and moving the high thumb past (low value plus the gap) must floor it there, writing the corrected value back to the slider immediately so the thumb visibly stops rather than crossing and snapping back.
- A small bar-chart histogram rendered above the slider from an array of counts per price band, where each bar's CSS class changes to a highlighted state when that band's center point falls within the currently selected range.
- Two number input fields (for typing an exact min and max) that stay in two-way sync with the sliders: dragging a thumb updates the number fields, and typing a value into a number field (on change, clamped to the same bounds and gap) moves the corresponding slider thumb and updates the fill and histogram.
- A live text readout showing the current selected range (e.g. "$120 to $640") that updates on every change from either input method.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

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.