noUiSlider Dual-Handle Price Range Filter — Free JS Snippet

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

CategoryForms

What's included

Features

Dual-handle range with a connecting bar and a minimum gap between handles
Step snapping to $5 and a custom to()/from() value format
Separate ariaFormat so screen readers announce dollars
Reads unencoded numeric values instead of parsing formatted strings
Product cards built once and toggled by class for smooth dragging
Live price histogram showing where products cluster
Two-way synced number inputs using noUiSlider.set([value, null])
Keyboard accessible handles with a visible focus ring

About this UI Snippet

noUiSlider Dual-Handle Price Range Filter — HTML, CSS & JavaScript

Screenshot of the noUiSlider Dual-Handle Price Range Filter snippet rendered live

Two handles on one track is the standard interface for filtering by price, but two native range inputs overlapped on top of each other is one of the most fragile hacks in front-end work — the handles cross, the fill needs manual maths and keyboard focus goes to whichever input is on top. noUiSlider was designed for exactly this problem: one slider, two handles, a connecting bar between them, with the crossing rules and step snapping handled inside the library.

This snippet builds a complete filter around it. The slider runs from 0 to 500 in steps of 5, with margin: 10 guaranteeing the handles can never sit closer than ten dollars apart. Tooltips show each handle's value, and the format option controls how they render — a to() function that adds the dollar sign and a from() function that strips it back off, so the library can parse typed input. ariaFormat does the same job for screen readers, announcing "40 dollars" instead of a bare number.

The important detail is which value the update listener uses. The first argument to update is an array of already formatted strings like "$40", which are for display. The third argument, unencoded, holds the raw numbers. The filter reads those, so there is no string parsing in the hot path. update fires continuously while dragging, so the product cards are built once and only a class is toggled on each tick; rebuilding the grid on every event would flicker and drop frames.

A price histogram sits behind the handles. Products are bucketed into twenty bins, and bars whose range overlaps the selection light up, which tells users at a glance where the products actually are — a range filter without that feedback invites selecting a range that matches nothing. The two number inputs are synced both ways: dragging updates them, and typing calls slider.noUiSlider.set([value, null]), where null leaves the other handle untouched.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Ask an AI assistant like Claude to add a non-linear range so cheaper products get more track space, persist the selected range in the URL query string, or add an "on sale only" checkbox that combines with the price filter.

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-handle price range filter with noUiSlider 15 loaded from a CDN.

Requirements:
- Create a slider from 0 to 500 with step 5, start at [40, 360], connect: true, margin: 10 and tooltips on both handles.
- Use a format object with to()/from() that adds and removes a "$" prefix, plus a separate ariaFormat announcing "dollars".
- In the update listener use the third argument (unencoded) as the numbers for filtering, not the formatted strings.
- Render a product grid once and toggle a class on cards outside the range, and show "x of y products match".
- Add a price histogram behind the slider that highlights bins inside the range, plus two number inputs synced with slider.noUiSlider.set([value, null]).

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.

Source Code

<div class="pr-wrap">
  <div class="pr-panel">
    <div class="pr-head"><h3>Price</h3><span id="prSummary">$40 – $360</span></div>
    <div class="pr-hist" id="prHist" aria-hidden="true"></div>
    <div id="prSlider"></div>
    <div class="pr-inputs">
      <label>Min <input type="number" id="prMin" min="0" max="500" step="5"></label>
      <label>Max <input type="number" id="prMax" min="0" max="500" step="5"></label>
      <button type="button" id="prReset">Reset</button>
    </div>
  </div>
  <div class="pr-results">
    <p class="pr-count" id="prCount" aria-live="polite"></p>
    <ul class="pr-grid" id="prGrid"></ul>
  </div>
</div>

Step by step

How to Use

  1. 1
    Drag a handleDrag either handle. The bar, the tooltip, the histogram and the product grid all update together.
  2. 2
    Watch the histogramBars inside your range turn teal, showing how many products live at each price point.
  3. 3
    Type exact valuesEnter a number in Min or Max. The slider moves to match and the other handle stays where it was.
  4. 4
    Try to cross the handlesDrag one handle towards the other — the ten-dollar margin stops them colliding.
  5. 5
    ResetPress Reset to restore the full 0 – 500 range and show every product.

Real-world uses

Common Use Cases

SHOP
E-commerce category pages
Filter a product grid by price without a page reload. Combine with typo-tolerant product search for a complete listing page.
Real-estate and travel search
Set a budget range for listings or flights, with a histogram showing market density.
Analytics date or value ranges
Use the same dual-handle pattern for any numeric window: age, score, latency or file size.
Learning range-slider internals
See how to keep formatted display values separate from raw numbers in an event-driven UI.

Got questions?

Frequently Asked Questions

Overlapping range inputs cannot cross safely and need manual fill maths. noUiSlider gives one component with connected handles, margins, snapping and keyboard support built in.

values are strings run through your format.to() function for display. unencoded are the raw numbers, which are what filtering logic should use.

Set the margin option. Here margin: 10 keeps them at least ten dollars apart.

Call slider.noUiSlider.set([min, null]) or set([null, max]). null leaves that handle unchanged.

noUiSlider handles are focusable and respond to arrow keys. Add an ariaFormat so spoken values include the unit.

Yes. Provide extra keys in range, such as "50%": [100, 10], to give the lower prices more track length.

Yes. Use the JSX, Vue, Angular or Tailwind export buttons on this page to convert the markup and styles. The behaviour comes from noUiSlider, so in a framework project install it with npm install nouislider instead of the CDN tag, create it in useEffect / onMounted / ngAfterViewInit on the element, and release it with noUiSlider destroy() when the component unmounts.