I have an almost working solution in the sandbox linked in the update below, you should start with this.
I have one that's created without using React (Click "Run Code Snippet")
var thumbsize = 14;
function draw(slider,splitvalue) {
/* set function vars */
var min = slider.querySelector('.min');
var max = slider.querySelector('.max');
var thumbsize = parseInt(slider.getAttribute('data-thumbsize'));
var rangewidth = parseInt(slider.getAttribute('data-rangewidth'));
var rangemin = parseInt(slider.getAttribute('data-rangemin'));
var rangemax = parseInt(slider.getAttribute('data-rangemax'));
/* set min and max attributes */
min.setAttribute('max',splitvalue);
max.setAttribute('min',splitvalue);
/* set css */
min.style.width = parseInt(thumbsize + ((splitvalue - rangemin)/(rangemax - rangemin))*(rangewidth - (2*thumbsize)))+'px';
max.style.width = parseInt(thumbsize + ((rangemax - splitvalue)/(rangemax - rangemin))*(rangewidth - (2*thumbsize)))+'px';
min.style.left = '0px';
max.style.left = parseInt(min.style.width)+'px';
/* correct for 1 off at the end */
if(max.value>(rangemax - 1)) max.setAttribute('data-value',rangemax);
/* write value and labels */
max.value = max.getAttribute('data-value');
min.value = min.getAttribute('data-value');
}
function init(slider) {
/* set function vars */
var min = slider.querySelector('.min');
var max = slider.querySelector('.max');
var rangemin = parseInt(min.getAttribute('min'));
var rangemax = parseInt(max.getAttribute('max'));
var avgvalue = (rangemin + rangemax)/2;
/* set data-values */
min.setAttribute('data-value',rangemin);
max.setAttribute('data-value',rangemax);
/* set data vars */
slider.setAttribute('data-rangemin',rangemin);
slider.setAttribute('data-rangemax',rangemax);
slider.setAttribute('data-thumbsize',thumbsize);
slider.setAttribute('data-rangewidth',slider.offsetWidth);
/* draw */
draw(slider,avgvalue);
/* events */
min.addEventListener("input", function() {update(min);});
max.addEventListener("input", function() {update(max);});
}
function update(el){
/* set function vars */
var slider = el.parentElement;
var min = slider.querySelector('#min');
var max = slider.querySelector('#max');
var minvalue = Math.floor(min.value);
var maxvalue = Math.floor(max.value);
/* set inactive values before draw */
min.setAttribute('data-value',minvalue);
max.setAttribute('data-value',maxvalue);
var avgvalue = (minvalue + maxvalue)/2;
/* draw */
draw(slider,avgvalue);
}
var sliders = document.querySelectorAll('.min-max-slider');
sliders.forEach( function(slider) {
init(slider);
});
.min-max-slider {
position: relative;
width: 200px;
text-align: center;
margin-bottom: 50px;
}
.min-max-slider > label {
display: none;
}
.min-max-slider > input {
cursor: pointer;
position: absolute;
}
/* webkit specific styling */
.min-max-slider > input {
-webkit-appearance: none;
outline: none!important;
background: transparent;
background-image: linear-gradient(to bottom, transparent 0%, transparent 30%, silver 30%, silver 60%, transparent 60%, transparent 100%);
}
.min-max-slider > input::-webkit-slider-thumb {
-webkit-appearance: none; /* Override default look */
appearance: none;
width: 14px; /* Set a specific slider handle width */
height: 14px; /* Slider handle height */
background: #eee; /* Green background */
cursor: pointer; /* Cursor on hover */
border: 1px solid gray;
border-radius: 100%;
}
.min-max-slider > input::-webkit-slider-runnable-track {cursor: pointer;}
<div class="min-max-slider" data-legendnum="2">
<label for="min">Minimum price</label>
<input id="min" class="min" name="min" type="range" step="1" min="0" max="3000" />
<label for="max">Maximum price</label>
<input id="max" class="max" name="max" type="range" step="1" min="0" max="3000" />
</div>
I've tried to create the same sort of component using React. But each thumbgrip can only slide to the mid-point, where as with the non-react example, the lesser slider simply can't be higher than the higher slider and vice versa.
I would also like some way to make the slider which outside the range of the thumbgrips a darker color.
var thumbsize = 14;
const Slider = ({min, max}) => {
const avg = (min + max)/2;
const width = 300
const minWidth = thumbsize + ((avg - min)/(max - min))*(width - (2*thumbsize))
const styles={
min:{
width: minWidth,
left: 0
},
max:{
width: thumbsize + ((max - avg)/(max - min))*(width - (2*thumbsize)),
left: minWidth
}
}
return (
<div
class="min-max-slider"
data-legendnum="2"
data-rangemin={min}
data-rangemax={max}
data-thumbsize={thumbsize}
data-rangewidth={width}
>
<label htmlFor="min">Minimum price</label>
<input
id="min"
className="min"
style={styles.min}
name="min"
type="range"
step="1"
min={min}
max={avg}
data-value={min}
/>
<label htmlFor="max">Maximum price</label>
<input
id="max"
className="max"
style={styles.max}
name="max"
type="range"
step="1"
min={avg}
max={max}
data-value={max}
/>
</div>
)
}
ReactDOM.render(<Slider min={300} max={3000} />, document.querySelector("#root"))
.min-max-slider {
position: relative;
width: 200px;
text-align: center;
margin-bottom: 50px;
}
.min-max-slider > label {
display: none;
}
.min-max-slider > input {
cursor: pointer;
position: absolute;
}
/* webkit specific styling */
.min-max-slider > input {
-webkit-appearance: none;
outline: none!important;
background: transparent;
background-image: linear-gradient(to bottom, transparent 0%, transparent 30%, silver 30%, silver 60%, transparent 60%, transparent 100%);
}
.min-max-slider > input::-webkit-slider-thumb {
-webkit-appearance: none; /* Override default look */
appearance: none;
width: 14px; /* Set a specific slider handle width */
height: 14px; /* Slider handle height */
background: #eee; /* Green background */
cursor: pointer; /* Cursor on hover */
border: 1px solid gray;
border-radius: 100%;
}
.min-max-slider > input::-webkit-slider-runnable-track {cursor: pointer;}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root"></div>
UPDATE
Using lissetdm's solution below, I have been able to create a double sided slider, which you can view on this sandbox, but there are still a few problems with it.
Sometimes, unpredictably, this difference is very noticeable
I have tested this out in firefox
As react works, you need to move avg to the component's state and during input onChange event update it's value:
const Slider = ({ min, max }) => {
const [avg, setAvg] = useState((min + max) / 2);
const [minVal, setMinVal] = useState(avg);
const [maxVal, setMaxVal] = useState(avg);
const width = 300;
const minWidth =
thumbsize + ((avg - min) / (max - min)) * (width - 2 * thumbsize);
const styles = {
min: {
width: minWidth,
left: 0
},
max: {
width: thumbsize + ((max - avg) / (max - min)) * (width - 2 * thumbsize),
left: minWidth
}
};
useEffect(() => {
setAvg((maxVal + minVal) / 2);
}, [minVal, maxVal]);
return (
<div
className="min-max-slider"
data-legendnum="2"
data-rangemin={min}
data-rangemax={max}
data-thumbsize={thumbsize}
data-rangewidth={width}
>
<label htmlFor="min">Minimum price</label>
<input
id="min"
className="min"
style={styles.min}
name="min"
type="range"
step="1"
min={min}
max={avg}
value={minVal}
onChange={({ target }) => setMinVal(Number(target.value))}
/>
<label htmlFor="max">Maximum price</label>
<input
id="max"
className="max"
style={styles.max}
name="max"
type="range"
step="1"
min={avg}
max={max}
value={maxVal}
onChange={({ target }) => setMaxVal(Number(target.value))}
/>
</div>
);
};
I can't seem to remove this outline, which doesn't always appear
This line appears because the browser detects that the input is invalid. To disable this effect use:
input[type="range"]:invalid {
box-shadow: none;
}
The orange doesn't always look like it overlaps the gray
And
When both thumb pieces are dragged to the edges, the one pushes the other down
I can't reproduce this scenarios but I recommend you to control the background color using the min and max position:
CSS:
input[type="range"] {
--minRangePercent: 0%;
--maxRangePercent: 0%;
height: .4rem;
}
.min-max-slider > input.min {
background-image: linear-gradient(
to right,
silver 0%,
silver var(--minRangePercent),
orange var(--minRangePercent),
orange 100%
);
}
.min-max-slider > input.max {
background-image: linear-gradient(
to right,
orange 0%,
orange var(--maxRangePercent),
silver var(--maxRangePercent),
silver 100%
);
}
JS:
const minPercent = ((minVal - min) / (avg - min)) * 100;
const maxPercent = ((maxVal - avg) / (max - avg)) * 100;
const styles = {
min: {
width: minWidth,
left: 0,
"--minRangePercent": `${minPercent}%`
},
max: {
width: thumbsize + ((max - avg) / (max - min)) * (width - 2 * thumbsize),
left: minWidth,
"--maxRangePercent": `${maxPercent}%`
}
};
Working example
The problem was that min
and max
are strings so calculating the avg
was incorrect but by using parseInt
you will get integers as expected. here i created a sandbox but i had to change the value of width
from 30
to 300
to make enough space for the dots.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With