164

Is it possible to make a HTML5 slider with two input values, for example to select a price range? If so, how can it be done?

isherwood
  • 58,414
  • 16
  • 114
  • 157
frequent
  • 27,643
  • 59
  • 181
  • 333
  • 4
    I've made a CSS-based component for this - [Codepen demo](https://codepen.io/vsync/pen/mdEJMLv) – vsync Jun 25 '21 at 11:48
  • So far of all these answers I believe only one is accessible and another one is _somewhat_ accessible though I have commented on that answer with it's accessibility issues. If you're reading this then it's at least 2022 by now and I implore you to find a fully accessible solution (`ctrl+f` "accessibility") – maxshuty Jan 03 '22 at 03:37
  • 1
    Adding to the more recent examples of how this can be done: [Native dual range slider](https://medium.com/@predragdavidovic10/native-dual-range-slider-html-css-javascript-91e778134816) – sketchyTech Aug 19 '22 at 09:13

11 Answers11

107

I've been looking for a lightweight, dependency free dual slider for some time (it seemed crazy to import jQuery just for this) and there don't seem to be many out there. I ended up modifying @Wildhoney's code a bit and really like it.

function getVals(){
  // Get slider values
  var parent = this.parentNode;
  var slides = parent.getElementsByTagName("input");
    var slide1 = parseFloat( slides[0].value );
    var slide2 = parseFloat( slides[1].value );
  // Neither slider will clip the other, so make sure we determine which is larger
  if( slide1 > slide2 ){ var tmp = slide2; slide2 = slide1; slide1 = tmp; }
  
  var displayElement = parent.getElementsByClassName("rangeValues")[0];
      displayElement.innerHTML = slide1 + " - " + slide2;
}

window.onload = function(){
  // Initialize Sliders
  var sliderSections = document.getElementsByClassName("range-slider");
      for( var x = 0; x < sliderSections.length; x++ ){
        var sliders = sliderSections[x].getElementsByTagName("input");
        for( var y = 0; y < sliders.length; y++ ){
          if( sliders[y].type ==="range" ){
            sliders[y].oninput = getVals;
            // Manually trigger event first time to display values
            sliders[y].oninput();
          }
        }
      }
}
  section.range-slider {
    position: relative;
    width: 200px;
    height: 35px;
    text-align: center;
}

section.range-slider input {
    pointer-events: none;
    position: absolute;
    overflow: hidden;
    left: 0;
    top: 15px;
    width: 200px;
    outline: none;
    height: 18px;
    margin: 0;
    padding: 0;
}

section.range-slider input::-webkit-slider-thumb {
    pointer-events: all;
    position: relative;
    z-index: 1;
    outline: 0;
}

section.range-slider input::-moz-range-thumb {
    pointer-events: all;
    position: relative;
    z-index: 10;
    -moz-appearance: none;
    width: 9px;
}

section.range-slider input::-moz-range-track {
    position: relative;
    z-index: -1;
    background-color: rgba(0, 0, 0, 1);
    border: 0;
}
section.range-slider input:last-of-type::-moz-range-track {
    -moz-appearance: none;
    background: none transparent;
    border: 0;
}
  section.range-slider input[type=range]::-moz-focus-outer {
  border: 0;
}
<!-- This block can be reused as many times as needed -->
<section class="range-slider">
  <span class="rangeValues"></span>
  <input value="5" min="0" max="15" step="0.5" type="range">
  <input value="10" min="0" max="15" step="0.5" type="range">
</section>
Gary
  • 13,303
  • 18
  • 49
  • 71
  • Unfortunately it does not work with Android 4.0.4 Mobile Safari 4.0 browser. :-( – erik Aug 14 '15 at 19:28
  • @erik I don't really have a good way to test those versions, but it works for me on Android 5.0 with the latest version of Safari (which google play says is 1.2, so I'm confused about your 4.0). If you figure it out, I'd love to know. – Gary Sep 07 '15 at 16:18
  • 1
    That's really clever! I wonder how it's not marked as the correct answer since it solves the problem gracefully without any kind of dependency. Adding jQuery just to do that is obviously overkill. – zanona Apr 30 '16 at 10:53
  • @zanona Thanks but the correct answer was marked 4 years prior, so I don't think the OP cared much for another solution. – Gary Apr 30 '16 at 14:20
  • @boldnik He would have gotten a notification of a new answer to his question, but again, I don't he's too concerned with it any more. For me, this was a case of not being able to find a solution and sharing what I came up with for the next guy. – Gary May 10 '16 at 17:31
  • 20
    I really like your approach and tried to adapt it to my use case but had to give up on it when i realized that some of the design specifications I have (e.g. coloring the "active" part of the range differently than the rest of the track) are not possible to implement with this. So i looked for another solution and found this really neat project: http://refreshless.com/nouislider/ It's dependency free, has a nice and clean API, is AMD compatible, and offers a lot of options. So far I'm quite happy with it. – Felix Wienberg May 12 '16 at 16:55
  • Thanks for posting this, this is clearly the most efficient solution using some 20 lines of JS. Adapted it and it works like a charm on Chrome, Safari & iOS which is all I have at hand so far. – SGD Aug 14 '17 at 14:19
  • This answer is not accessible and has less customizations available, for those who need more customization and accessibility (which everyone needs!) see [this answer](https://stackoverflow.com/a/70561454/4826740) – maxshuty Apr 04 '23 at 15:31
87

No, the HTML5 range input only accepts one input. I would recommend you to use something like the jQuery UI range slider for that task.

Jarno
  • 6,243
  • 3
  • 42
  • 57
Martin Buberl
  • 45,844
  • 25
  • 100
  • 144
  • Thanks for the link and info. I have to check whether I can get this to run on mobile devices. – frequent Jan 21 '11 at 08:11
  • 47
    Some time ago... but I managed to "fake" a double slider by placing two sliders exactly on top of each other. One starting at min-value, the other starting at max-value. I guess that's cheating but... it works for me. – frequent Jul 23 '11 at 07:43
  • WhatWG is at least discussing implementing it: https://html.spec.whatwg.org/multipage/forms.html#range-state-%28type=range%29:attr-input-multiple-3 – kunambi Feb 26 '15 at 13:32
  • 9
    I like ionRangeSlider a little better than the jQuery UI slider myself: http://ionden.com/a/plugins/ion.rangeSlider/en.html – Charlotte May 18 '16 at 14:45
  • @frequent, I'd like to fake the double slider your way, but if I draw two elements on top of one another, only the top one is accepting mouse clicks. – Ross Rogers Aug 10 '17 at 16:04
  • 5
    Another approach is to "fake" the double slider with side-by-side input controls: http://www.simple.gy/blog/range-slider-two-handles/ – SimplGy Nov 26 '18 at 07:38
  • I have created a similar solution to the one from SimplGy. You can find it here: https://stackoverflow.com/a/64612997/2397550 – Mr. Hugo Oct 30 '20 at 17:39
  • @kunambi It seems nothing came out of those discussions. :-( – Mitar Sep 19 '22 at 18:32
73

Coming late, but noUiSlider avoids having a jQuery-ui dependency, which the accepted answer does not. Its only "caveat" is IE support is for IE9 and newer, if legacy IE is a deal breaker for you.

It's also free, open source and can be used in commercial projects without restrictions.

Installation: Download noUiSlider, extract the CSS and JS file somewhere in your site file system, and then link to the CSS from head and to JS from body:

<!-- In <head> -->
<link href="nouislider.min.css" rel="stylesheet">

<!-- In <body> -->
<script src="nouislider.min.js"></script>

Example usage: Creates a slider which goes from 0 to 100, and starts set to 20-80.

HTML:

<div id="slider">
</div>

JS:

var slider = document.getElementById('slider');

noUiSlider.create(slider, {
    start: [20, 80],
    connect: true,
    range: {
        'min': 0,
        'max': 100
    }
});
dario_ramos
  • 7,118
  • 9
  • 61
  • 108
  • 2
    WOW, amazing!! This is exactly what we needed and it is wonderfully developed! WHITOUT jQuery – DavidTaubmann Mar 26 '17 at 00:51
  • It should be fairly easy to add that yourself and send a pull request to the project, though? – dario_ramos May 25 '18 at 19:38
  • 3
    I didn't dig into it, but I could use the slider with the keyboard, so I think it's implemented now @ptrin :) – Edu Ruiz Sep 30 '19 at 14:45
  • Thank you very much for this wonderful answer. Nothing is late, just share your answer when you found it. – dhiraj Dec 22 '20 at 04:41
  • Perfect. I'm having problems adding features from [noUISlider documentation such as tooltips](https://refreshless.com/nouislider/number-formatting/#section-tooltips) and adding `tooltips: [true, true],` does nothing. Ideas? Need we do more than link to nouislider.min.css and nouislider.min.js? – for_all_intensive_purposes Mar 25 '21 at 00:25
  • I downloaded the [latest release](https://github.com/leongersen/noUiSlider/releases) but there is no "nouislider.min.js". Over 200 files but not really sure how I'm supposed to use it in my own project. – Luke May 27 '21 at 01:22
  • @Luke Seems like the files are called nouislider.css and nouislider.js in the latest version; confirm and I'll update the answer – dario_ramos May 31 '21 at 04:16
  • 1
    @ChrisDixon (Sorry, just saw the comment) That should be enough, maybe try debugging tooltips and confirm they are broken. If you can fix them, you can send a PR to the project ;) – dario_ramos May 31 '21 at 04:20
  • Thanks. And for those using vuejs, http://veeno.surge.sh is based on nouislider, and works great too. – italo.portinho Oct 19 '21 at 12:11
36

Sure you can simply use two sliders overlaying each other and add a bit of javascript (actually not more than 5 lines) that the selectors are not exceeding the min/max values (like in @Garys) solution.

Attached you'll find a short snippet adapted from a current project including some CSS3 styling to show what you can do (webkit only). I also added some labels to display the selected values.

It uses JQuery but a vanillajs version is no magic though.

@Update: The code below was just a proof of concept. Due to many requests I've added a possible solution for Mozilla Firefox (without changing the original code). You may want to refractor the code below before using it.

    (function() {

        function addSeparator(nStr) {
            nStr += '';
            var x = nStr.split('.');
            var x1 = x[0];
            var x2 = x.length > 1 ? '.' + x[1] : '';
            var rgx = /(\d+)(\d{3})/;
            while (rgx.test(x1)) {
                x1 = x1.replace(rgx, '$1' + '.' + '$2');
            }
            return x1 + x2;
        }

        function rangeInputChangeEventHandler(e){
            var rangeGroup = $(this).attr('name'),
                minBtn = $(this).parent().children('.min'),
                maxBtn = $(this).parent().children('.max'),
                range_min = $(this).parent().children('.range_min'),
                range_max = $(this).parent().children('.range_max'),
                minVal = parseInt($(minBtn).val()),
                maxVal = parseInt($(maxBtn).val()),
                origin = $(this).context.className;

            if(origin === 'min' && minVal > maxVal-5){
                $(minBtn).val(maxVal-5);
            }
            var minVal = parseInt($(minBtn).val());
            $(range_min).html(addSeparator(minVal*1000) + ' €');


            if(origin === 'max' && maxVal-5 < minVal){
                $(maxBtn).val(5+ minVal);
            }
            var maxVal = parseInt($(maxBtn).val());
            $(range_max).html(addSeparator(maxVal*1000) + ' €');
        }

     $('input[type="range"]').on( 'input', rangeInputChangeEventHandler);
})();
body{
font-family: sans-serif;
font-size:14px;
}
input[type='range'] {
  width: 210px;
  height: 30px;
  overflow: hidden;
  cursor: pointer;
    outline: none;
}
input[type='range'],
input[type='range']::-webkit-slider-runnable-track,
input[type='range']::-webkit-slider-thumb {
  -webkit-appearance: none;
    background: none;
}
input[type='range']::-webkit-slider-runnable-track {
  width: 200px;
  height: 1px;
  background: #003D7C;
}

input[type='range']:nth-child(2)::-webkit-slider-runnable-track{
  background: none;
}

input[type='range']::-webkit-slider-thumb {
  position: relative;
  height: 15px;
  width: 15px;
  margin-top: -7px;
  background: #fff;
  border: 1px solid #003D7C;
  border-radius: 25px;
  z-index: 1;
}


input[type='range']:nth-child(1)::-webkit-slider-thumb{
  z-index: 2;
}

.rangeslider{
    position: relative;
    height: 60px;
    width: 210px;
    display: inline-block;
    margin-top: -5px;
    margin-left: 20px;
}
.rangeslider input{
    position: absolute;
}
.rangeslider{
    position: absolute;
}

.rangeslider span{
    position: absolute;
    margin-top: 30px;
    left: 0;
}

.rangeslider .right{
   position: relative;
   float: right;
   margin-right: -5px;
}


/* Proof of concept for Firefox */
@-moz-document url-prefix() {
  .rangeslider::before{
    content:'';
    width:100%;
    height:2px;
    background: #003D7C;
    display:block;
    position: relative;
    top:16px;
  }

  input[type='range']:nth-child(1){
    position:absolute;
    top:35px !important;
    overflow:visible !important;
    height:0;
  }

  input[type='range']:nth-child(2){
    position:absolute;
    top:35px !important;
    overflow:visible !important;
    height:0;
  }
input[type='range']::-moz-range-thumb {
  position: relative;
  height: 15px;
  width: 15px;
  margin-top: -7px;
  background: #fff;
  border: 1px solid #003D7C;
  border-radius: 25px;
  z-index: 1;
}

  input[type='range']:nth-child(1)::-moz-range-thumb {
      transform: translateY(-20px);    
  }
  input[type='range']:nth-child(2)::-moz-range-thumb {
      transform: translateY(-20px);    
  }
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<div class="rangeslider">
                                <input class="min" name="range_1" type="range" min="1" max="100" value="10" />
                                <input class="max" name="range_1" type="range" min="1" max="100" value="90" />
                                <span class="range_min light left">10.000 €</span>
                                <span class="range_max light right">90.000 €</span>
                            </div>
user1532132
  • 827
  • 2
  • 10
  • 19
  • 7
    Hey looks great! but it doesn't work in Firefox, you can only dragg the max input. Did you manage to fix it? – Toni Michel Caubet May 21 '18 at 18:29
  • 6
    Oh man. I came here to add almost exactly the same thing! http://www.simple.gy/blog/range-slider-two-handles/ My version puts the inputs side-by-side, but also uses dual inputs. – SimplGy Nov 26 '18 at 07:34
  • I've now added a proof of concept for Firefox – user1532132 Feb 03 '21 at 11:40
  • 3
    Hello there! I found this useful, and wanted to contribute for others reading in the future. As of jQuery 3.0, this no longer works. The line `origin = $(this).context.className;` should be changed to `origin = e.originalEvent.target.className;`, or an equivalent. The `.context` feature was deprecated in 1.10, and removed in 3.0 – Austen Holland Apr 30 '21 at 02:28
  • This is exactly how I was thinking I'd handle a dual range slider. Thanks, saved me a lot of trial and error! – Bernesto Jul 20 '22 at 20:28
28

Actually I used my script in html directly. But in javascript when you add oninput event listener for this event it gives the data automatically.You just need to assign the value as per your requirement.

[slider] {
  width: 300px;
  position: relative;
  height: 5px;
  margin: 45px 0 10px 0;
}

[slider] > div {
  position: absolute;
  left: 13px;
  right: 15px;
  height: 5px;
}
[slider] > div > [inverse-left] {
  position: absolute;
  left: 0;
  height: 5px;
  border-radius: 10px;
  background-color: #CCC;
  margin: 0 7px;
}

[slider] > div > [inverse-right] {
  position: absolute;
  right: 0;
  height: 5px;
  border-radius: 10px;
  background-color: #CCC;
  margin: 0 7px;
}


[slider] > div > [range] {
  position: absolute;
  left: 0;
  height: 5px;
  border-radius: 14px;
  background-color: #d02128;
}

[slider] > div > [thumb] {
  position: absolute;
  top: -7px;
  z-index: 2;
  height: 20px;
  width: 20px;
  text-align: left;
  margin-left: -11px;
  cursor: pointer;
  box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4);
  background-color: #FFF;
  border-radius: 50%;
  outline: none;
}

[slider] > input[type=range] {
  position: absolute;
  pointer-events: none;
  -webkit-appearance: none;
  z-index: 3;
  height: 14px;
  top: -2px;
  width: 100%;
  opacity: 0;
}

div[slider] > input[type=range]:focus::-webkit-slider-runnable-track {
  background: transparent;
  border: transparent;
}

div[slider] > input[type=range]:focus {
  outline: none;
}

div[slider] > input[type=range]::-webkit-slider-thumb {
  pointer-events: all;
  width: 28px;
  height: 28px;
  border-radius: 0px;
  border: 0 none;
  background: red;
  -webkit-appearance: none;
}

div[slider] > input[type=range]::-ms-fill-lower {
  background: transparent;
  border: 0 none;
}

div[slider] > input[type=range]::-ms-fill-upper {
  background: transparent;
  border: 0 none;
}

div[slider] > input[type=range]::-ms-tooltip {
  display: none;
}

[slider] > div > [sign] {
  opacity: 0;
  position: absolute;
  margin-left: -11px;
  top: -39px;
  z-index:3;
  background-color: #d02128;
  color: #fff;
  width: 28px;
  height: 28px;
  border-radius: 28px;
  -webkit-border-radius: 28px;
  align-items: center;
  -webkit-justify-content: center;
  justify-content: center;
  text-align: center;
}

[slider] > div > [sign]:after {
  position: absolute;
  content: '';
  left: 0;
  border-radius: 16px;
  top: 19px;
  border-left: 14px solid transparent;
  border-right: 14px solid transparent;
  border-top-width: 16px;
  border-top-style: solid;
  border-top-color: #d02128;
}

[slider] > div > [sign] > span {
  font-size: 12px;
  font-weight: 700;
  line-height: 28px;
}

[slider]:hover > div > [sign] {
  opacity: 1;
}
<div slider id="slider-distance">
  <div>
    <div inverse-left style="width:70%;"></div>
    <div inverse-right style="width:70%;"></div>
    <div range style="left:0%;right:0%;"></div>
    <span thumb style="left:0%;"></span>
    <span thumb style="left:100%;"></span>
    <div sign style="left:0%;">
      <span id="value">0</span>
    </div>
    <div sign style="left:100%;">
      <span id="value">100</span>
    </div>
  </div>
  <input type="range" value="0" max="100" min="0" step="1" oninput="
  this.value=Math.min(this.value,this.parentNode.childNodes[5].value-1);
  let value = (this.value/parseInt(this.max))*100
  var children = this.parentNode.childNodes[1].childNodes;
  children[1].style.width=value+'%';
  children[5].style.left=value+'%';
  children[7].style.left=value+'%';children[11].style.left=value+'%';
  children[11].childNodes[1].innerHTML=this.value;" />

  <input type="range" value="100" max="100" min="0" step="1" oninput="
  this.value=Math.max(this.value,this.parentNode.childNodes[3].value-(-1));
  let value = (this.value/parseInt(this.max))*100
  var children = this.parentNode.childNodes[1].childNodes;
  children[3].style.width=(100-value)+'%';
  children[5].style.right=(100-value)+'%';
  children[9].style.left=value+'%';children[13].style.left=value+'%';
  children[13].childNodes[1].innerHTML=this.value;" />
</div>
Himabindu
  • 634
  • 8
  • 22
  • 1
    You are a genius! This is an awesome idea and your styling is also great. – Puspam Oct 25 '20 at 20:48
  • 1
    I've been looking for days - this is an excellent solution! Thank you for sharing!! – dmboucher Jun 19 '21 at 20:57
  • There goes my 10, but I don't know why I have problems dragging in mozilla, if we add input[type=range]::-moz-range-thumb then there are problems with edge, any idea? – Braian Mellor May 10 '22 at 22:14
25

The question was: "Is it possible to make a HTML5 slider with two input values, for example to select a price range? If so, how can it be done?"

In 2020 it is possible to create a fully accessible, native, non-jquery HTML5 slider with two thumbs for price ranges. If found this posted after I already created this solution and I thought that it would be nice to share my implementation here.

This implementation has been tested on mobile Chrome and Firefox (Android) and Chrome and Firefox (Linux). I am not sure about other platforms, but it should be quite good. I would love to get your feedback and improve this solution.

This solution allows multiple instances on one page and it consists of just two inputs (each) with descriptive labels for screen readers. You can set the thumb size in the amount of grid labels. Also, you can use touch, keyboard and mouse to interact with the slider. The value is updated during adjustment, due to the 'on input' event listener.

My first approach was to overlay the sliders and clip them. However, that resulted in complex code with a lot of browser dependencies. Then I recreated the solution with two sliders that were 'inline'. This is the solution you will find below.

var thumbsize = 14;

function draw(slider,splitvalue) {

    /* set function vars */
    var min = slider.querySelector('.min');
    var max = slider.querySelector('.max');
    var lower = slider.querySelector('.lower');
    var upper = slider.querySelector('.upper');
    var legend = slider.querySelector('.legend');
    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';
    min.style.top = lower.offsetHeight+'px';
    max.style.top = lower.offsetHeight+'px';
    legend.style.marginTop = min.offsetHeight+'px';
    slider.style.height = (lower.offsetHeight + min.offsetHeight + legend.offsetHeight)+'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');
    lower.innerHTML = min.getAttribute('data-value');
    upper.innerHTML = max.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;
    var legendnum = slider.getAttribute('data-legendnum');

    /* 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);

    /* write labels */
    var lower = document.createElement('span');
    var upper = document.createElement('span');
    lower.classList.add('lower','value');
    upper.classList.add('upper','value');
    lower.appendChild(document.createTextNode(rangemin));
    upper.appendChild(document.createTextNode(rangemax));
    slider.insertBefore(lower,min.previousElementSibling);
    slider.insertBefore(upper,min.previousElementSibling);
    
    /* write legend */
    var legend = document.createElement('div');
    legend.classList.add('legend');
    var legendvalues = [];
    for (var i = 0; i < legendnum; i++) {
        legendvalues[i] = document.createElement('div');
        var val = Math.round(rangemin+(i/(legendnum-1))*(rangemax - rangemin));
        legendvalues[i].appendChild(document.createTextNode(val));
        legend.appendChild(legendvalues[i]);

    } 
    slider.appendChild(legend);

    /* 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);
});
* {padding: 0; margin: 0;}
body {padding: 40px;}

.min-max-slider {position: relative; width: 200px; text-align: center; margin-bottom: 50px;}
.min-max-slider > label {display: none;}
span.value {height: 1.7em; font-weight: bold; display: inline-block;}
span.value.lower::before {content: "€"; display: inline-block;}
span.value.upper::before {content: "- €"; display: inline-block; margin-left: 0.4em;}
.min-max-slider > .legend {display: flex; justify-content: space-between;}
.min-max-slider > .legend > * {font-size: small; opacity: 0.25;}
.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>

Note that you should keep the step size to 1 to prevent the values to change due to redraws/redraw bugs.

View online at: https://codepen.io/joosts/pen/rNLdxvK

Mr. Hugo
  • 11,887
  • 3
  • 42
  • 60
  • 1
    Thank you, I really like the solution! – Svatopluk Ledl Oct 31 '20 at 17:48
  • I've had problem with the rounding though (`avgvalue` can be XX.5). As a result you can create range of min size 1 with left slider but 0 with right slider. I've solved it with `floor`/`ceil` for related input's `max`/`min` + conditional `padding-left` for second slider thus allowing range of min size 1 from both sides. – Svatopluk Ledl Oct 31 '20 at 17:59
  • 11
    It's a clever code, but I wouldn't say "times have changed", this is still using two sliders and still adding JS and Css to overcome the standard shortcomings. In my mind, "Time of changed" would be: to support a multi value input element. or something. – JAR.JAR.beans Jan 19 '21 at 07:26
  • I agree with this solution however I'm with problems when I try to update the values with javascript. Anyone can help me to update the slider dynamically? – Marco Teixeira Jun 28 '21 at 14:30
  • Love that this is _mostly_ accessible, the only thing I believe should be done to make it more accessible is to add an outline, color change, or some other indicator to let a user know visually that they are adjusting either side of the slide. This should happen on focus so that it works when tabbing and using the arrow keys too. – maxshuty Dec 27 '21 at 14:59
  • Also I think this will cause the `label`s to not be read out by many screen readers: `.min-max-slider > label { display: none; }` – maxshuty Dec 27 '21 at 21:07
  • FYI I built a small library based off of this answer, you can see my answer and implementation here that fixes some of the issues I've mentioned in comments above: https://stackoverflow.com/a/70561454/4826740 – maxshuty Jan 03 '22 at 03:34
19

2023 - Accessible solution - 30 second solution to implement

I created a simple library for this, or you can apply the code yourself from this answer.

This solution builds off of this answer by @Mr. Hugo. Accessibility is something none of the answers have focused on and that is a problem, so I built off of the above answer by making it more accessible & extensible since it had some flaws.

Usage is very simple:

  1. Use the CDN or host the script locally: https://cdn.jsdelivr.net/gh/maxshuty/accessible-web-components/dist/simpleRange.min.js
  2. Add this element to your template or HTML: <range-selector min-range="0" max-range="1000" />
  3. Hook into it by listening for the range-changed event (or whatever event-name-to-emit-on-change you pass in)

That's it. View the full demo here. You can easily customize it by simply applying attributes like inputs-for-labels to use inputs instead of labels, slider-color to adjust the color, and so much more!

Here is a fiddle:

window.addEventListener('range-changed', (e) => {console.log(`Range changed for: ${e.detail.sliderId}. Min/Max range values are available in this object too`)})
<script src="https://cdn.jsdelivr.net/gh/maxshuty/accessible-web-components@latest/dist/simpleRange.min.js"></script>

<div>
  <range-selector
    id="rangeSelector1"
    min-label="Minimum"
    max-label="Maximum"
    min-range="1000"
    max-range="2022"
    number-of-legend-items-to-show="6"
  />
</div>

<div>
  <range-selector
    id="rangeSelector1"
    min-label="Minimum"
    max-label="Maximum"
    min-range="1"
    max-range="500"
    number-of-legend-items-to-show="3"
    inputs-for-labels
  />
</div>

<div>
  <range-selector
    id="rangeSelector2"
    min-label="Minimum"
    max-label="Maximum"
    min-range="1000"
    max-range="2022"
    number-of-legend-items-to-show="3"
    slider-color="#6b5b95"
  />
</div>

<div>
  <range-selector
    id="rangeSelector3"
    min-label="Minimum"
    max-label="Maximum"
    min-range="1000"
    max-range="2022"
    hide-label
    hide-legend
  />
</div>

I decided to address the issues of the linked answer like the labels using display: none (bad for a11y), no visual focus on the slider, etc., and improve the code by cleaning up event listeners and making it much more dynamic and extensible.

I created this tiny library with many options to customize colors, event names, easily hook into it, make the accessible labels i18n capable and much more. Here it is in a fiddle if you want to play around.

You can easily customize the number of legend items it shows, hide or show the labels and legend, and customize the colors of everything, including the focus color like this.

Example using several of the props:

<range-selector
  min-label="i18n Minimum Range"
  max-label="i18n Maximum Range"
  min-range="5"
  max-range="555"
  number-of-legend-items-to-show="6"
  event-name-to-emit-on-change="my-custom-range-changed-event"
  slider-color="orange"
  circle-color="#f7cac9"
  circle-border-color="#083535"
  circle-focus-border-color="#3ec400"
/>

Then in your script:

window.addEventListener('my-custom-range-changed-event', (e) => { const data = e.detail; });

Finally if you see that this is missing something that you need I made it very easy to customize this library.

Simply copy this file and at the top you can see cssHelpers and constants objects that contain most of the variables you would likely want to further customize.

Since I built this with a Native Web Component I have taken advantage of disconnectedCallback and other hooks to clean up event listeners and set things up.

maxshuty
  • 9,708
  • 13
  • 64
  • 77
2

Here is a reusable double range slider implementation, base on tutorial Double Range Slider by Coding Artist

  • near native UI, Chrome/Firefox/Safari compatible
  • API EventTarget based, with change/input events, minGap/maxGap properties

let $ = (s, c = document) => c.querySelector(s);
let $$ = (s, c = document) => Array.prototype.slice.call(c.querySelectorAll(s));

class DoubleRangeSlider extends EventTarget {
  #minGap = 0;
  #maxGap = Number.MAX_SAFE_INTEGER;
  #inputs;
  style = {
    trackColor: '#dadae5',
    rangeColor: '#3264fe',
  };
  constructor(container){
    super();
    let inputs = $$('input[type="range"]', container);
    if(inputs.length !== 2){
      throw new RangeError('2 range inputs expected');
    }
    let [input1, input2] = inputs;
    if(input1.min >= input1.max || input2.min >= input2.max){
      throw new RangeError('range min should be less than max');
    }
    if(input1.max > input2.max || input1.min > input2.min){
      throw new RangeError('input1\'s max/min should not be greater than input2\'s max/min');
    }
    this.#inputs = inputs;
    let sliderTrack = $('.slider-track', container);
    let lastValue1 = input1.value;
    input1.addEventListener('input', (e) => {
      let value1 = +input1.value;
      let value2 = +input2.value;
      let minGap = this.#minGap;
      let maxGap = this.#maxGap;
      let gap = value2 - value1;
      let newValue1 = value1;
      if(gap < minGap){
        newValue1 = value2 - minGap;
      }else if(gap > maxGap){
        newValue1 = value2 - maxGap;
      }
      input1.value = newValue1;
      if(input1.value !== lastValue1){
        lastValue1 = input1.value;
        passEvent(e);
        fillColor();
      }
    });
    let lastValue2 = input2.value;
    input2.addEventListener('input', (e) => {
      let value1 = +input1.value;
      let value2 = +input2.value;
      let minGap = this.#minGap;
      let maxGap = this.#maxGap;
      let gap = value2 - value1;
      let newValue2 = value2;
      if(gap < minGap){
        newValue2 = value1 + minGap;
      }else if(gap > maxGap){
        newValue2 = value1 + maxGap;
      }
      input2.value = newValue2;
      if(input2.value !== lastValue2){
        lastValue2 = input2.value;
        passEvent(e);
        fillColor();
      }
    });
    let passEvent = (e) => {
      this.dispatchEvent(new e.constructor(e.type, e));
    };
    input1.addEventListener('change', passEvent);
    input2.addEventListener('change', passEvent);
    let fillColor = () => {
      let overallMax = +input2.max;
      let overallMin = +input1.min;
      let overallRange = overallMax - overallMin;
      let left1 = ((input1.value - overallMin) / overallRange * 100) + '%';
      let left2 = ((input2.value - overallMin) / overallRange * 100) + '%';
      let {trackColor, rangeColor} = this.style;
      sliderTrack.style.background = `linear-gradient(to right, ${trackColor} ${left1}, ${rangeColor} ${left1}, ${rangeColor} ${left2}, ${trackColor} ${left2})`;
    };
    let init = () => {
      let overallMax = +input2.max;
      let overallMin = +input1.min;
      let overallRange = overallMax - overallMin;
      let range1 = input1.max - overallMin;
      let range2 = overallMax - input2.min;
      input1.style.left = '0px';
      input1.style.width = (range1 / overallRange * 100) + '%';
      input2.style.right = '0px';
      input2.style.width = (range2 / overallRange * 100) + '%';
      fillColor();
    };
    init();
  }
  get minGap(){
    return this.#minGap;
  }
  set minGap(v){
    this.#minGap = v;
  }
  get maxGap(){
    return this.#maxGap;
  }
  set maxGap(v){
    this.#maxGap = v;
  }
  get values(){
    return this.#inputs.map((el) => el.value);
  }
  set values(values){
    if(values.length !== 2 || !values.every(isFinite))
      throw new RangeError();
    let [input1, input2] = this.#inputs;
    let [value1, value2] = values;
    if(value1 > input1.max || value1 < input1.min)
      throw new RangeError('invalid value for input1');
    if(value2 > input2.max || value2 < input2.min)
      throw new RangeError('invalid value for input2');
    input1.value = value1;
    input2.value = value2;
  }
  get inputs(){
    return this.#inputs;
  }
  get overallMin(){
    return this.#inputs[0].min;
  }
  get overallMax(){
    return this.#inputs[1].max;
  }
}


function main(){
  let container = $('.slider-container');
  let slider = new DoubleRangeSlider(container);
  slider.minGap = 30;
  slider.maxGap = 70;

  let inputs = $$('input[name="a"]');
  let outputs = $$('output[name="a"]');
  outputs[0].value = inputs[0].value;
  outputs[1].value = inputs[1].value;

  slider.addEventListener('input', (e) => {
    let values = slider.values;
    outputs[0].value = values[0];
    outputs[1].value = values[1];
  });
  slider.addEventListener('change', (e) => {
    let values = slider.values;
    console.log('change', values);
    outputs[0].value = values[0];
    outputs[1].value = values[1];
  });
}
document.addEventListener('DOMContentLoaded', main);
.slider-container {
  display: inline-block;
  position: relative;
  width: 360px;
  height: 28px;
}
.slider-track {
  width: 100%;
  height: 5px;
  position: absolute;
  margin: auto;
  top: 0;
  bottom: 0;
  border-radius: 5px;
}
.slider-container>input[type="range"] {
  -webkit-appearance: none;
  -moz-appearance: none;
  appearance: none;
  position: absolute;
  margin: auto;
  top: 0;
  bottom: 0;
  width: 100%;
  outline: none;
  background-color: transparent;
  pointer-events: none;
}
.slider-container>input[type="range"]::-webkit-slider-runnable-track {
  -webkit-appearance: none;
  height: 5px;
}
.slider-container>input[type="range"]::-moz-range-track {
  -moz-appearance: none;
  height: 5px;
}
.slider-container>input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  margin-top: -9px;
  height: 1.7em;
  width: 1.7em;
  background-color: #3264fe;
  cursor: pointer;
  pointer-events: auto;
  border-radius: 50%;
}
.slider-container>input[type="range"]::-moz-range-thumb {
  -moz-appearance: none;
  height: 1.7em;
  width: 1.7em;
  cursor: pointer;
  border: none;
  border-radius: 50%;
  background-color: #3264fe;
  pointer-events: auto;
}
.slider-container>input[type="range"]:active::-webkit-slider-thumb {
  background-color: #ffffff;
  border: 3px solid #3264fe;
}
<h3>Double Range Slider, Reusable Edition</h3>

<div class="slider-container">
  <div class="slider-track"></div>
  <input type="range" name="a" min="-130" max="-30" step="1" value="-100" autocomplete="off" />
  <input type="range" name="a" min="-60" max="0" step="2" value="-30" autocomplete="off" />
</div>

<div>
  <output name="a"></output> ~ <output name="a"></output>
</div>

<pre>
Changes:
1. allow different min/max/step for two inputs
2. new property 'maxGap'
3. added events 'input'/'change'
4. dropped IE/OldEdge support
</pre>
fuweichin
  • 1,398
  • 13
  • 14
  • The inputs are not centered on Safari. It is fine with Chrome and Firefox. `top: -2px;` works but in that case, they are not centered on Chrome/Firefox. – wp-ap Aug 21 '22 at 21:31
  • I changed heights from em to px and now it looks good on Safari too. This is the simplest yet the best solution. Thank you! – wp-ap Aug 21 '22 at 23:22
1

For those working with Vue, there is now Veeno available, based on noUiSlider. But it does not seem to be maintained anymore. :-(

Mitar
  • 6,756
  • 5
  • 54
  • 86
1

Pure CSS solution

    <html>    
        <head>        
            <meta http-equiv="content-type" content="text/html; charset=utf-8">        
                <style>            
                    .multi-ranges {
                        position: relative;
                        width: 800px;
                        height: 20px;
                        background-color: rgb(220,220,220);
                        border: 1px solid rgb(0,0,0);
                    }
                    input[type=range] {
                        -webkit-appearance: none;
                        position: absolute;
                        top: 8px;
                        left: -2px;
                        width: 100%;
                        height: 0;
                        outline: none;
                    }
                    .multi-range::-webkit-slider-thumb {
                        -webkit-appearance: none;
                        position: relative;
                        width: 20px;
                        height: 20px;
                    }
                    #range1::-webkit-slider-thumb {
                        background-color: rgb(255,0,0);
                        z-index: 2;
                    }
                    #range2::-webkit-slider-thumb {
                        background-color: rgb(0,255,0);
                        z-index: 3;
                    }
                    #range3::-webkit-slider-thumb {
                        background-color: rgb(0,0,255);
                        z-index: 4;
                    }
                    #range1:hover::-webkit-slider-thumb,
                    #range2:hover::-webkit-slider-thumb,
                    #range3:hover::-webkit-slider-thumb {
                        z-index: 5;
                        background-color: rgb(255,215,0);
                    }
                    .multi-range::-moz-range-thumb {
                        position: relative;
                        width: 20px;
                        height: 20px;
                        border: 0 none;
                        border-radius: 0;
                    }
                    #range1::-moz-range-thumb {
                        background-color: rgb(255,0,0);
                    }
                    #range2::-moz-range-thumb {
                        background-color: rgb(0,255,0);
                    }
                    #range3::-moz-range-thumb {
                        background-color: rgb(0,0,255);
                    }
                    #range1:hover::-moz-range-thumb,
                    #range2:hover::-moz-range-thumb,
                    #range3:hover::-moz-range-thumb {
                        background-color: rgb(255,215,0);
                    }
                </style>        
            <title>Multi-range         
            </title>
    <script type="text/javascript">
    function range_value(fn1,fn2) {document.getElementById('range-value').value = 'RANGE#' + fn1 + ' [' + fn2.value + ']';}
    </script>    
        </head>    
        <body>        
            <div>            
                <output id="range-value">              
                </output>        
            </div>        
            <div class="multi-ranges">            
                <input type="range" id="range1" class="multi-range" min="0" max="100" value=0 step="1" oninput="range_value(1,this);">            
                <input type="range" id="range2" class="multi-range" min="0" max="100" value=50" step="1" oninput="range_value(2,this);">            
                <input type="range" id="range3" class="multi-range" min="0" max="100" value=100 step="1" oninput="range_value(3,this);">        
            </div>    
        </body>
    </html>
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Apr 19 '23 at 13:27
-1

This code covers following points

  1. Dual slider using HTML, CSS, JS

  2. I have modified this slider using embedded ruby so we can save previously applied values using params in rails.

     <% left_width = params[:min].nil? ? 0 : ((params[:min].to_f/100000) * 100).to_i %>
     <% left_value = params[:min].nil? ? '0' : params[:min] %>
     <% right_width = params[:max].nil? ? 100 : ((params[:max].to_f/100000) * 100).to_i %>
     <% right_value = params[:max].nil? ? '100000' : params[:max] %>
    
     <div class="range-slider-outer">
       <div slider id="slider-distance">
         <div class="slider-inner">
           <div inverse-left style="width:<%= left_width %>%;"></div>
           <div inverse-right style="width:<%= 100 - right_width %>%;"></div>
           <div range style="left:<%= left_width %>%;right:<%= 100 - right_width %>%;"></div>
           <span thumb style="left:<%= left_width %>%;"></span>
           <span thumb style="left:<%= right_width %>%;"></span>
           <div sign style="">
             Rs.<span id="value"><%= left_value.to_i %></span> to
           </div>
           <div sign style="">
             Rs.<span id="value"><%= right_value.to_i %></span>
           </div>
         </div>
    
         <input type="range" name="min" value=<%= left_value %> max="100000" min="0" step="100" oninput="
         this.value=Math.min(this.value,this.parentNode.childNodes[5].value-1);
         let value = (this.value/parseInt(this.max))*100
         var children = this.parentNode.childNodes[1].childNodes;
         children[1].style.width=value+'%';
         children[5].style.left=value+'%';
         children[7].style.left=value+'%';children[11].style.left=value+'%';
         children[11].childNodes[1].innerHTML=this.value;" />
    
         <input type="range" name="max" value=<%= right_value %> max="100000" min="0" step="100" oninput="
         this.value=Math.max(this.value,this.parentNode.childNodes[3].value-(-1));
         let value = (this.value/parseInt(this.max))*100
         var children = this.parentNode.childNodes[1].childNodes;
         children[3].style.width=(100-value)+'%';
         children[5].style.right=(100-value)+'%';
         children[9].style.left=value+'%';children[13].style.left=value+'%';
         children[13].childNodes[1].innerHTML=this.value;" />
       </div>
       <div class="range-label">
         <div>0</div>
         <div>100000</div>
       </div>
     </div>
    

[slider] {
  /*width: 300px;*/
  position: relative;
  height: 5px;
  /*margin: 20px auto;*/
  /* height: 100%; */
}
[slider] > div {
  position: absolute;
  left: 13px;
  right: 15px;
  height: 14px;
  top: 5px;
}
[slider] > div > [inverse-left] {
  position: absolute;
  left: 0;
  height: 14px;
  border-radius: 3px;
  background-color: #CCC;
  /*margin: 0 7px;*/
  margin: 0 -7px;
}
[slider] > div > [inverse-right] {
  position: absolute;
  right: 0;
  height: 14px;
  border-radius: 3px;
  background-color: #CCC;
  /*margin: 0 7px;*/
  margin: 0 -7px;
}
[slider] > div > [range] {
  position: absolute;
  left: 0;
  height: 14px;
  border-radius: 14px;
  background-color:#8950fc;
}
[slider] > div > [thumb] {
  position: absolute;
  top: -3px;
  z-index: 2;
  height: 20px;
  width: 20px;
  text-align: left;
  margin-left: -11px;
  cursor: pointer;
  /* box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4); */
  background-color: #FFF;
  /*border-radius: 50%;*/
  border-radius:2px;
  outline: none;
}
[slider] > input[type=range] {
  position: absolute;
  pointer-events: none;
  -webkit-appearance: none;
  z-index: 3;
  height: 14px;
  top: -2px;
  width: 100%;
  opacity: 0;
}
div[slider] > input[type=range]:focus::-webkit-slider-runnable-track {
  background: transparent;
  border: transparent;
}
div[slider] > input[type=range]:focus {
  outline: none;
}
div[slider] > input[type=range]::-webkit-slider-thumb {
  pointer-events: all;
  width: 28px;
  height: 28px;
  border-radius: 0px;
  border: 0 none;
  background: red;
  -webkit-appearance: none;
}
div[slider] > input[type=range]::-ms-fill-lower {
  background: transparent;
  border: 0 none;
}
div[slider] > input[type=range]::-ms-fill-upper {
  background: transparent;
  border: 0 none;
}
div[slider] > input[type=range]::-ms-tooltip {
  display: none;
}
[slider] > div > [sign] {
 /* opacity: 0;
  position: absolute;
  margin-left: -11px;
  top: -39px;
  z-index:3;
  background-color:#1a243a;
  color: #fff;
  width: 28px;
  height: 28px;
  border-radius: 28px;
  -webkit-border-radius: 28px;
  align-items: center;
  -webkit-justify-content: center;
  justify-content: center;
  text-align: center;*/
    color: #A5B2CB;
    border-radius: 28px;
    justify-content: center;
    text-align: center;
    display: inline-block;
    margin-top: 12px;
    font-size: 14px;
    font-weight: bold;
}
.slider-inner{
  text-align:center;
}
/*[slider] > div > [sign]:after {
  position: absolute;
  content: '';
  left: 0;
  border-radius: 16px;
  top: 19px;
  border-left: 14px solid transparent;
  border-right: 14px solid transparent;
  border-top-width: 16px;
  border-top-style: solid;
  border-top-color:#1a243a;
}*/
[slider] > div > [sign] > span {
  font-size: 12px;
  font-weight: 700;
  line-height: 28px;
}
[slider]:hover > div > [sign] {
  opacity: 1;
}
.range-label{
  display: flex;
  justify-content: space-between;
  margin-top: 28px;
  padding: 0px 5px;
}
.range-slider-outer{
  width:calc(100% - 20px);
  margin:auto;
  margin-bottom: 10px;
  margin-top: 10px;
}
Rushikesh k
  • 1
  • 1
  • 1