DoubleRange - Vanilla JavaScript Dual-Thumb Range Slider - DEMO page
Example 1. Basic
<div id="slider-1"></div>
const formatterFn = (v) => `$ ${v}`;
const callbackFn = (from,to) => {console.log(`example1 from:${from} to:${to}`)};
const slider = DoubleRange.create({
selector: "#slider-1",
min: 30,
max: 500,
from: 100,
to: 400,
step: 1,
label: 'Example 1',
formatter: formatterFn,
delay: 300,
callback: callbackFn
});
Example 2. Basic CSS customization
<div id="slider-2"></div>
const toEuro = (n) => n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.') + ' €';
const callbackFn = (from,to) => {console.log(`example2 from:${from} to:${to}`)};
const slider = DoubleRange.create({
selector: "#slider-2",
min: 1015,
max: 23050,
from: 10000,
to: 20000,
step: 1,
label: 'Example 2',
formatter: toEuro,
delay: 500,
callback: callbackFn
});
#slider-2 .double-range {
--thumb-from-bg: #145056;
--thumb-to-bg: #95be08;
--range-bar-bg: linear-gradient(to right, #0c2e32, #145056);
--thumb-size: 20px;
/* custom padding , calc(var(--thumb-size) / 2) is the max for 100% width*/
padding: 30px calc(var(--thumb-size) / 2);
overflow: visible;
}
#slider-2 .double-range label
{
font-size: 13px;
color:#222222;
padding: 0px;
}
Example 3. CSS:Without start and end point JS:ensure that from and to have at least 100 distance
<div id="slider-3"></div>
let slider = null;
const beforeFromChangeFn = (from, to) => {
if (to - from < 100) {
slider.setFrom(to - 100);
return false;
}
return true;
};
const beforeToChangeFn = (from, to) => {
if (to - from < 100) {
slider.setTo(from + 100);
return false;
}
return true;
};
const formatterFn = (v) => `$ ${v}`;
const callbackFn = (from, to) => { console.log(`example3 from:${from} to:${to}`) };
slider = DoubleRange.create({
selector: "#slider-3",
min: 30,
max: 500,
from: 100,
to: 400,
step: 1,
label: 'Example 3',
formatter: formatterFn,
beforeFromChange:beforeFromChangeFn,
beforeToChange:beforeToChangeFn,
delay: 250,
callback: callbackFn
});
#slider-3 .double-range {
--thumb-from-bg: #003377;
--thumb-to-bg: #005599;
--range-bar-bg: linear-gradient(to right, #003377, #005599);
padding: 40px calc(var(--thumb-size) / 2);
--thumb-size: 30px;
overflow: visible;
}
#slider-3 .double-range label
{
font-size: 13px;
color:#222222;
padding: 0px;
}
#slider-3 .double-range .point {
display: none;
}
#slider-3 .double-range .track
{
background: none;
box-shadow: inherit;
}
/* instead of the track line */
#slider-3 .double-range::before {
position: absolute;
display: inline-block;
content: "";
left: 0px;
top: calc(50% - 5px);
width: 100%;
height: 10px;
background: linear-gradient(to right, #e1e3e7, #f1f3f7);
border-radius: 5px;
cursor: pointer;
box-shadow: 0px 0px 1px 1px rgba(100, 100, 100, 0.2);
}
Example 4. Date range
<div id="slider-4"></div>
const dateFormatter = (local) => {
const intl = new Intl.DateTimeFormat(local);
const parts = intl.formatToParts(new Date(2000, 0, 1))
.filter(p => p.type !== 'literal');
const order = parts.reduce((o, p, i) => (o[p.type] = i, o), {});
const msPerDay = 864e5;
return {
fromInt: v => intl.format(new Date(v * msPerDay)),
toInt: s => {
const v = s.split(/\D+/);
const y = +v[order.year];
const m = +v[order.month] - 1;
const d = +v[order.day];
return Date.UTC(y, m, d) / msPerDay | 0;
}
};
};
const formatter = dateFormatter("en-US");
const callbackFn = (from, to) => {
console.log(
`example:4 from:${formatter.fromInt(from)} to:${formatter.fromInt(to)}`);
};
const slider = DoubleRange.create({
selector: "#slider-4",
min: formatter.toInt("01/01/2026"),
max: formatter.toInt("12/31/2026"),
from: formatter.toInt("03/01/2026"),
to: formatter.toInt("05/15/2026"),
step: 1,
label: 'Example 4',
formatter: formatter.fromInt,
delay: 500,
callback: callbackFn
});
#slider-4 .double-range {
--thumb-from-bg: #005599;
--thumb-to-bg: #990055;
--range-bar-bg: linear-gradient(to right, #cee1f8, #f8cec1);
}
#slider-4 .double-range label
{
font-size: 13px;
text-shadow:0px 0px 1px;
letter-spacing:1px;
padding: 0px;
}
#slider-4 .double-range label.from
{
color:#005599;
}
#slider-4 .double-range label.from::after
{
color:#000000;
}
#slider-4 .double-range label.to
{
color:#990022;
}
Example 5. setFrom() , setTo() , setMin() , setMax()
<div class="fields">
<div>
<input type="text" id="from-5" placeholder="" size="6" maxlength="6">
<button id="set-from-5">setFrom()</button>
</div>
<div>
<input type="text" id="to-5" placeholder="" size="6" maxlength="6">
<button id="set-to-5">setTo()</button>
</div>
<div>
<input type="text" id="min-5" placeholder="" size="6" maxlength="6">
<button id="set-min-5">setMin()</button>
</div>
<div>
<input type="text" id="max-5" placeholder="" size="6" maxlength="6">
<button id="set-max-5">setMax()</button>
</div>
<div>
<label for='callback-5'>result:</label>
<input type="text" id="result-5" placeholder="" size="6" maxlength="6">
</div>
</div>
const inp = {
from: document.getElementById('from-5'),
to: document.getElementById('to-5'),
min: document.getElementById('min-5'),
max: document.getElementById('max-5'),
result: document.getElementById('result-5')
}
const btn = {
from: document.getElementById('set-from-5'),
to: document.getElementById('set-to-5'),
min: document.getElementById('set-min-5'),
max: document.getElementById('set-max-5'),
};
const toEuro = (n) => n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, '.') + ' €';
const callbackFn = (from, to) => inp.result.value = `from:${from} to:${to}`;
const slider = DoubleRange.create({
selector: "#slider-5",
min: 5400,
max: 19600,
from: 8398,
to: 15325,
step: 1,
label: 'Example 5',
formatter: toEuro,
delay: 150,
callback: callbackFn
});
const pointError = (el) => {
el.value = "";
el.classList.add("error");
setTimeout(()=>{el.classList.remove("error");},300);
};
btn.from.addEventListener('click', () => {
if (!slider.setFrom(parseInt(inp.from.value))) {
// setFrom and setTo return false if the value is invalid
pointError(inp.from);
}
});
btn.to.addEventListener('click', () => {
if (!slider.setTo(parseInt(inp.to.value))) {
pointError(inp.to);
}
});
btn.min.addEventListener('click', () => {
// setMin and setMax throw an error if the value is invalid
try {
slider.setMin(parseInt(inp.min.value));
}
catch (e) {
console.error(e);
pointError(inp.min);
}
});
btn.max.addEventListener('click', () => {
try {
slider.setMax(parseInt(inp.max.value));
}
catch (e) {
console.error(e);
pointError(inp.max);
}
});
#slider-5 .double-range label
{
font-size: 13px;
padding: 0px;
text-shadow:0px 0px 1px;
letter-spacing: 1px;
}
#slider-5 .double-range label.from>span
{
color:#06203b;
}
#slider-5 .double-range label.to>span
{
color:#194a7e;
}