Price Range Slider — Free HTML CSS JS Dual Handle Range Slider Snippet
Price Range Slider · Forms · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="range-widget">
<div class="range-labels">
<span>Price Range</span>
<span class="range-value" id="rangeValue">$20 - $180</span>
</div>
<div class="range-track-wrap">
<div class="range-track"></div>
<div class="range-fill" id="rangeFill"></div>
<input type="range" min="0" max="200" value="20" step="1" class="range-input range-min" id="minRange" aria-label="Minimum price">
<input type="range" min="0" max="200" value="180" step="1" class="range-input range-max" id="maxRange" aria-label="Maximum price">
</div>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f8fafc; padding: 40px; display: flex; align-items: center; flex-direction: column; gap: 20px; justify-content: center; min-height: 100vh; }
.range-widget { max-width: 320px; }
.range-labels {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.range-labels span:first-child { font-size: 13px; font-weight: 600; color: #475569; }
.range-value { font-size: 13px; font-weight: 700; color: #6366f1; background: #eef2ff; padding: 4px 10px; border-radius: 999px; }
.range-track-wrap { position: relative; height: 32px; }
.range-track {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 4px;
background: #e2e8f0;
border-radius: 999px;
transform: translateY(-50%);
}
.range-fill {
position: absolute;
top: 50%;
height: 4px;
background: #6366f1;
border-radius: 999px;
transform: translateY(-50%);
}
.range-input {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 32px;
margin: 0;
background: transparent;
pointer-events: none;
-webkit-appearance: none;
appearance: none;
}
.range-input::-webkit-slider-thumb {
-webkit-appearance: none;
pointer-events: auto;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
border: 3px solid #6366f1;
cursor: pointer;
box-shadow: 0 1px 4px rgba(15,23,42,0.2);
margin-top: 7px;
}
.range-input::-moz-range-thumb {
pointer-events: auto;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
border: 3px solid #6366f1;
cursor: pointer;
box-shadow: 0 1px 4px rgba(15,23,42,0.2);
}
.range-input:focus-visible::-webkit-slider-thumb { outline: 2px solid #6366f1; outline-offset: 2px; }const minRange = document.getElementById('minRange');
const maxRange = document.getElementById('maxRange');
const fill = document.getElementById('rangeFill');
const valueDisplay = document.getElementById('rangeValue');
const MAX = 200;
const GAP = 5;
function updateSlider() {
let minVal = parseInt(minRange.value, 10);
let maxVal = parseInt(maxRange.value, 10);
if (maxVal - minVal < GAP) {
if (this === minRange) {
minVal = maxVal - GAP;
minRange.value = minVal;
} else {
maxVal = minVal + GAP;
maxRange.value = maxVal;
}
}
const minPercent = (minVal / MAX) * 100;
const maxPercent = (maxVal / MAX) * 100;
fill.style.left = minPercent + '%';
fill.style.width = (maxPercent - minPercent) + '%';
valueDisplay.textContent = '$' + minVal + ' - $' + maxVal;
}
minRange.addEventListener('input', updateSlider);
maxRange.addEventListener('input', updateSlider);
updateSlider();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.
What's included
Features
About this UI Snippet
Price Range Slider — HTML, CSS & JavaScript Dual-Thumb Range Filter

Filtering products by a min/max price range is a staple of e-commerce filter sidebars. Building it usually means reaching for a slider plugin — but a convincing dual-handle range slider can be built with two overlapping native <input type="range"> elements and a small amount of JavaScript to keep them from crossing.
How two range inputs simulate one dual-handle slider
Both #minRange and #maxRange are absolutely positioned on top of each other, spanning the same track. Each is styled with a transparent background and pointer-events: none on the input track itself, but the ::-webkit-slider-thumb/::-moz-range-thumb pseudo-elements get pointer-events: auto. This means clicking anywhere on the invisible track does nothing, but clicking and dragging either visible circular thumb works normally — the two inputs never fight over the same click because only their thumbs are interactive, and the thumbs sit at different positions.
How the colored fill bar is calculated
A separate .range-fill div (not part of either input) is positioned absolutely between the two thumbs. updateSlider() converts both values to percentages of the slider's max ((value / MAX) * 100) and sets the fill's left to the min percentage and width to the difference between max and min percentages. This is recalculated on every input event from either slider, so the fill visually tracks both thumbs in real time.
How the handles are prevented from crossing
Nothing stops a native range input from having its value larger than a sibling's on its own — the two inputs know nothing about each other. updateSlider checks whether maxVal - minVal has dropped below a minimum GAP, and if so, it forces the *other* slider's value back to maintain that gap, using this (the slider that just fired the event) to determine which one to leave alone and which one to push. This keeps a small buffer between the handles so they never visually overlap or invert.
Why two separate inputs instead of a single custom-built slider
Reusing native <input type="range"> elements means keyboard support (arrow keys, Page Up/Down, Home/End), touch dragging, and screen reader value announcements all come for free from the browser — a fully custom-built slider would need to reimplement all of that manually.
Step by step
How to Use
- 1Load the snippetClick "Price Range Slider" in the sidebar Library tab to load the dual-handle slider.
- 2Drag both handlesDrag the left and right thumbs in the preview and watch the fill bar and "$X - $Y" label update live.
- 3Test the minimum gapTry dragging one handle into the other — notice it stops before crossing, maintaining a small gap.
- 4Change the price rangeUpdate the min/max attributes on both range inputs and the MAX constant in JS to match your real price bounds.
- 5Adjust the minimum gapChange the GAP constant in the JS panel to allow the handles to get closer or further apart.
- 6Export and saveExport as HTML/JSX/Tailwind or click "Save as" to reuse this filter in a real product listing page.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Both inputs are absolutely positioned on top of the same track with transparent backgrounds. Only their thumb pseudo-elements have pointer-events enabled, so users can drag either thumb independently while the invisible track underneath ignores clicks.
The updateSlider function checks the gap between the current min and max values after every input event. If the gap falls below a configured minimum, it pushes the slider that did not just move back to restore that minimum gap, preventing the handles from passing through each other.
Both slider values are converted to percentages of the maximum range value. The fill's left offset is set to the minimum percentage and its width is set to the difference between the max and min percentages, so it always spans exactly between the two visible thumbs.
Native range inputs come with built-in keyboard support, touch dragging, and screen reader value announcements for free. A fully custom slider built from plain divs would need to reimplement all of that accessibility and interaction behavior manually.
Update the min and max attributes on both #minRange and #maxRange inputs in the HTML, and update the matching MAX constant in the JavaScript panel so the fill-bar percentage math stays correct.
Yes — change the string concatenation in updateSlider's valueDisplay.textContent line to use your currency symbol or a proper Intl.NumberFormat currency formatter.
Yes — native range inputs have built-in touch drag support in all modern mobile browsers, so the thumbs can be dragged with a finger exactly as they are with a mouse.