onchange event on input type=range is not triggering in firefox while dragging

Apparently Chrome and Safari are wrong: onchange should only be triggered when the user releases the mouse. To get continuous updates, you should use the oninput event, which will capture live updates in Firefox, Safari and Chrome, both from the mouse and the keyboard.

However, oninput is not supported in IE10, so your best bet is to combine the two event handlers, like this:

<span id="valBox"></span>
<input type="range" min="5" max="10" step="1" 
   oninput="showVal(this.value)" onchange="showVal(this.value)">

Check out this Bugzilla thread for more information.


SUMMARY:

I provide here a no-jQuery cross-browser desktop-and-mobile ability to consistently respond to range/slider interactions, something not possible in current browsers. It essentially forces all browsers to emulate IE11's on("change"... event for either their on("change"... or on("input"... events. The new function is...

function onRangeChange(r,f) {
  var n,c,m;
  r.addEventListener("input",function(e){n=1;c=e.target.value;if(c!=m)f(e);m=c;});
  r.addEventListener("change",function(e){if(!n)f(e);});
}

...where r is your range input element and f is your listener. The listener will be called after any interaction that changes the range/slider value but not after interactions that do not change that value, including initial mouse or touch interactions at the current slider position or upon moving off either end of the slider.

Problem:

As of early June 2016, different browsers differ in terms of how they respond to range/slider usage. Five scenarios are relevant:

  1. initial mouse-down (or touch-start) at the current slider position
  2. initial mouse-down (or touch-start) at a new slider position
  3. any subsequent mouse (or touch) movement after 1 or 2 along the slider
  4. any subsequent mouse (or touch) movement after 1 or 2 past either end of the slider
  5. final mouse-up (or touch-end)

The following table shows how at least three different desktop browsers differ in their behaviour with respect to which of the above scenarios they respond to:

table showing browser differences with respect to which events they respond to and when

Solution:

The onRangeChange function provides a consistent and predictable cross-browser response to range/slider interactions. It forces all browsers to behave according to the following table:

table showing behaviour of all browsers using the proposed solution

In IE11, the code essentially allows everything to operate as per the status quo, i.e. it allows the "change" event to function in its standard way and the "input" event is irrelevant as it never fires anyway. In other browsers, the "change" event is effectively silenced (to prevent extra and sometimes not-readily-apparent events from firing). In addition, the "input" event fires its listener only when the range/slider's value changes. For some browsers (e.g. Firefox) this occurs because the listener is effectively silenced in scenarios 1, 4 and 5 from the above list.

(If you truly require a listener to be activated in either scenario 1, 4 and/or 5 you could try incorporating "mousedown"/"touchstart", "mousemove"/"touchmove" and/or "mouseup"/"touchend" events. Such a solution is beyond the scope of this answer.)

Functionality in Mobile Browsers:

I have tested this code in desktop browsers but not in any mobile browsers. However, in another answer on this page MBourne has shown that my solution here "...appears to work in every browser I could find (Win desktop: IE, Chrome, Opera, FF; Android Chrome, Opera and FF, iOS Safari)". (Thanks MBourne.)

Usage:

To use this solution, include the onRangeChange function from the summary above (simplified/minified) or the demo code snippet below (functionally identical but more self-explanatory) in your own code. Invoke it as follows:

onRangeChange(myRangeInputElmt, myListener);

where myRangeInputElmt is your desired <input type="range"> DOM element and myListener is the listener/handler function you want invoked upon "change"-like events.

Your listener may be parameter-less if desired or may use the event parameter, i.e. either of the following would work, depending on your needs:

var myListener = function() {...

or

var myListener = function(evt) {...

(Removing the event listener from the input element (e.g. using removeEventListener) is not addressed in this answer.)

Demo Description:

In the code snippet below, the function onRangeChange provides the universal solution. The rest of the code is simply an example to demonstrate its use. Any variable that begins with my... is irrelevant to the universal solution and is only present for the sake of the demo.

The demo shows the range/slider value as well as the number of times the standard "change", "input" and custom "onRangeChange" events have fired (rows A, B and C respectively). When running this snippet in different browsers, note the following as you interact with the range/slider:

  • In IE11, the values in rows A and C both change in scenarios 2 and 3 above while row B never changes.
  • In Chrome and Safari, the values in rows B and C both change in scenarios 2 and 3 while row A changes only for scenario 5.
  • In Firefox, the value in row A changes only for scenario 5, row B changes for all five scenarios, and row C changes only for scenarios 2 and 3.
  • In all of the above browsers, the changes in row C (the proposed solution) are identical, i.e. only for scenarios 2 and 3.

Demo Code:

// main function for emulating IE11's "change" event:

function onRangeChange(rangeInputElmt, listener) {

  var inputEvtHasNeverFired = true;

  var rangeValue = {current: undefined, mostRecent: undefined};
  
  rangeInputElmt.addEventListener("input", function(evt) {
    inputEvtHasNeverFired = false;
    rangeValue.current = evt.target.value;
    if (rangeValue.current !== rangeValue.mostRecent) {
      listener(evt);
    }
    rangeValue.mostRecent = rangeValue.current;
  });

  rangeInputElmt.addEventListener("change", function(evt) {
    if (inputEvtHasNeverFired) {
      listener(evt);
    }
  }); 

}

// example usage:

var myRangeInputElmt = document.querySelector("input"          );
var myRangeValPar    = document.querySelector("#rangeValPar"   );
var myNumChgEvtsCell = document.querySelector("#numChgEvtsCell");
var myNumInpEvtsCell = document.querySelector("#numInpEvtsCell");
var myNumCusEvtsCell = document.querySelector("#numCusEvtsCell");

var myNumEvts = {input: 0, change: 0, custom: 0};

var myUpdate = function() {
  myNumChgEvtsCell.innerHTML = myNumEvts["change"];
  myNumInpEvtsCell.innerHTML = myNumEvts["input" ];
  myNumCusEvtsCell.innerHTML = myNumEvts["custom"];
};

["input", "change"].forEach(function(myEvtType) {
  myRangeInputElmt.addEventListener(myEvtType,  function() {
    myNumEvts[myEvtType] += 1;
    myUpdate();
  });
});

var myListener = function(myEvt) {
  myNumEvts["custom"] += 1;
  myRangeValPar.innerHTML = "range value: " + myEvt.target.value;
  myUpdate();
};

onRangeChange(myRangeInputElmt, myListener);
table {
  border-collapse: collapse;  
}
th, td {
  text-align: left;
  border: solid black 1px;
  padding: 5px 15px;
}
<input type="range"/>
<p id="rangeValPar">range value: 50</p>
<table>
  <tr><th>row</th><th>event type                     </th><th>number of events    </th><tr>
  <tr><td>A</td><td>standard "change" events         </td><td id="numChgEvtsCell">0</td></tr>
  <tr><td>B</td><td>standard "input" events          </td><td id="numInpEvtsCell">0</td></tr>
  <tr><td>C</td><td>new custom "onRangeChange" events</td><td id="numCusEvtsCell">0</td></tr>
</table>

Credit:

While the implementation here is largely my own, it was inspired by MBourne's answer. That other answer suggested that the "input" and "change" events could be merged and that the resulting code would work in both desktop and mobile browsers. However, the code in that answer results in hidden "extra" events being fired, which in and of itself is problematic, and the events fired differ between browsers, a further problem. My implementation here solves those problems.

Keywords:

JavaScript input type range slider events change input browser compatability cross-browser desktop mobile no-jQuery


I'm posting this as an answer because it deserves to be it's own answer rather than a comment under a less useful answer. I find this method much better than the accepted answer since it can keep all the js in a separate file from the HTML.

Answer provided by Jamrelian in his comment under the accepted answer.

$("#myelement").on("input change", function() {
    //do something
});

Just be aware of this comment by Jaime though

Just note that with this solution, in chrome you will get two calls to the handler (one per event), so if you care for that, then you need to guard against it.

As in it will fire the event when you have stopped moving the mouse, and then again when you release the mouse button.

Update

In relation to the change event and input event causing the functionality to trigger twice, this is pretty much a non-issue.

If you have a function firing off on input, it is extremely unlikely that there will be a problem when the change event fires.

input fires rapidly as you drag a range input slider. Worrying about one more function call firing at the end is like worrying about a single drop of water compared to the ocean of water that is the input events.

The reason for even including the change event at all is for the sake of browser compatibility (mainly with IE).


UPDATE: I am leaving this answer here as an example of how to use mouse events to use range/slider interactions in desktop (but not mobile) browsers. However, I have now also written a completely different and, I believe, better answer elsewhere on this page that uses a different approach to providing a cross-browser desktop-and-mobile solution to this problem.

Original answer:

Summary: A cross-browser, plain JavaScript (i.e. no-jQuery) solution to allow reading range input values without using on('input'... and/or on('change'... which work inconsistently between browsers.

As of today (late Feb, 2016), there is still browser inconsistency so I'm providing a new work-around here.

The problem: When using a range input, i.e. a slider, on('input'... provides continuously updated range values in Mac and Windows Firefox, Chrome and Opera as well as Mac Safari, while on('change'... only reports the range value upon mouse-up. In contrast, in Internet Explorer (v11), on('input'... does not work at all, and on('change'... is continuously updated.

I report here 2 strategies to get identical continuous range value reporting in all browsers using vanilla JavaScript (i.e. no jQuery) by using the mousedown, mousemove and (possibly) mouseup events.

Strategy 1: Shorter but less efficient

If you prefer shorter code over more efficient code, you can use this 1st solution which uses mousesdown and mousemove but not mouseup. This reads the slider as needed, but continues firing unnecessarily during any mouse-over events, even when the user has not clicked and is thus not dragging the slider. It essentially reads the range value both after 'mousedown' and during 'mousemove' events, slightly delaying each using requestAnimationFrame.

var rng = document.querySelector("input");

read("mousedown");
read("mousemove");
read("keydown"); // include this to also allow keyboard control

function read(evtType) {
  rng.addEventListener(evtType, function() {
    window.requestAnimationFrame(function () {
      document.querySelector("div").innerHTML = rng.value;
      rng.setAttribute("aria-valuenow", rng.value); // include for accessibility
    });
  });
}
<div>50</div><input type="range"/>

Strategy 2: Longer but more efficient

If you need more efficient code and can tolerate longer code length, then you can use the following solution which uses mousedown, mousemove and mouseup. This also reads the slider as needed, but appropriately stops reading it as soon as the mouse button is released. The essential difference is that is only starts listening for 'mousemove' after 'mousedown', and it stops listening for 'mousemove' after 'mouseup'.

var rng = document.querySelector("input");

var listener = function() {
  window.requestAnimationFrame(function() {
    document.querySelector("div").innerHTML = rng.value;
  });
};

rng.addEventListener("mousedown", function() {
  listener();
  rng.addEventListener("mousemove", listener);
});
rng.addEventListener("mouseup", function() {
  rng.removeEventListener("mousemove", listener);
});

// include the following line to maintain accessibility
// by allowing the listener to also be fired for
// appropriate keyboard events
rng.addEventListener("keydown", listener);
<div>50</div><input type="range"/>

Demo: Fuller explanation of the need for, and implementation of, the above work-arounds

The following code more fully demonstrates numerous aspects of this strategy. Explanations are embedded in the demonstration:

var select, inp, listen, unlisten, anim, show, onInp, onChg, onDn1, onDn2, onMv1, onMv2, onUp, onMvCombo1, onDnCombo1, onUpCombo2, onMvCombo2, onDnCombo2;

select   = function(selctr)     { return document.querySelector(selctr);      };
inp = select("input");
listen   = function(evtTyp, cb) { return inp.   addEventListener(evtTyp, cb); };
unlisten = function(evtTyp, cb) { return inp.removeEventListener(evtTyp, cb); };
anim     = function(cb)         { return window.requestAnimationFrame(cb);    };
show = function(id) {
	return function() {
    select("#" + id + " td~td~td"   ).innerHTML = inp.value;
    select("#" + id + " td~td~td~td").innerHTML = (Math.random() * 1e20).toString(36); // random text
  };
};

onInp      =                  show("inp" )                                      ;
onChg      =                  show("chg" )                                      ;
onDn1      =                  show("mdn1")                                      ;
onDn2      = function() {anim(show("mdn2"));                                   };
onMv1      =                  show("mmv1")                                      ;
onMv2      = function() {anim(show("mmv2"));                                   };
onUp       =                  show("mup" )                                      ;
onMvCombo1 = function() {anim(show("cmb1"));                                   };
onDnCombo1 = function() {anim(show("cmb1"));   listen("mousemove", onMvCombo1);};
onUpCombo2 = function() {                    unlisten("mousemove", onMvCombo2);};
onMvCombo2 = function() {anim(show("cmb2"));                                   };
onDnCombo2 = function() {anim(show("cmb2"));   listen("mousemove", onMvCombo2);};

listen("input"    , onInp     );
listen("change"   , onChg     );
listen("mousedown", onDn1     );
listen("mousedown", onDn2     );
listen("mousemove", onMv1     );
listen("mousemove", onMv2     );
listen("mouseup"  , onUp      );
listen("mousedown", onDnCombo1);
listen("mousedown", onDnCombo2);
listen("mouseup"  , onUpCombo2);
table {border-collapse: collapse; font: 10pt Courier;}
th, td {border: solid black 1px; padding: 0 0.5em;}
input {margin: 2em;}
li {padding-bottom: 1em;}
<p>Click on 'Full page' to see the demonstration properly.</p>
<table>
  <tr><th></th><th>event</th><th>range value</th><th>random update indicator</th></tr>
  <tr id="inp" ><td>A</td><td>input                                </td><td>100</td><td>-</td></tr>
  <tr id="chg" ><td>B</td><td>change                               </td><td>100</td><td>-</td></tr>
  <tr id="mdn1"><td>C</td><td>mousedown                            </td><td>100</td><td>-</td></tr>
  <tr id="mdn2"><td>D</td><td>mousedown using requestAnimationFrame</td><td>100</td><td>-</td></tr>
  <tr id="mmv1"><td>E</td><td>mousemove                            </td><td>100</td><td>-</td></tr>
  <tr id="mmv2"><td>F</td><td>mousemove using requestAnimationFrame</td><td>100</td><td>-</td></tr>
  <tr id="mup" ><td>G</td><td>mouseup                              </td><td>100</td><td>-</td></tr>
  <tr id="cmb1"><td>H</td><td>mousedown/move combo                 </td><td>100</td><td>-</td></tr>
  <tr id="cmb2"><td>I</td><td>mousedown/move/up combo              </td><td>100</td><td>-</td></tr>
</table>
<input type="range" min="100" max="999" value="100"/>
<ol>
  <li>The 'range value' column shows the value of the 'value' attribute of the range-type input, i.e. the slider. The 'random update indicator' column shows random text as an indicator of whether events are being actively fired and handled.</li>
  <li>To see browser differences between input and change event implementations, use the slider in different browsers and compare A and&nbsp;B.</li>
  <li>To see the importance of 'requestAnimationFrame' on 'mousedown', click a new location on the slider and compare C&nbsp;(incorrect) and D&nbsp;(correct).</li>
  <li>To see the importance of 'requestAnimationFrame' on 'mousemove', click and drag but do not release the slider, and compare E&nbsp;(often 1&nbsp;pixel behind) and F&nbsp;(correct).</li>
  <li>To see why an initial mousedown is required (i.e. to see why mousemove alone is insufficient), click and hold but do not drag the slider and compare E&nbsp;(incorrect), F&nbsp;(incorrect) and H&nbsp;(correct).</li>
  <li>To see how the mouse event combinations can provide a work-around for continuous update of a range-type input, use the slider in any manner and note whichever of A or B continuously updates the range value in your current browser. Then, while still using the slider, note that H and I provide the same continuously updated range value readings as A or B.</li>
  <li>To see how the mouseup event reduces unnecessary calculations in the work-around, use the slider in any manner and compare H and&nbsp;I. They both provide correct range value readings. However, then ensure the mouse is released (i.e. not clicked) and move it over the slider without clicking and notice the ongoing updates in the third table column for H but not&nbsp;I.</li>
</ol>

Andrew Willem's solutions are not mobile device compatible.

Here's a modification of his second solution that works in Edge, IE, Opera, FF, Chrome, iOS Safari and mobile equivalents (that I could test):

Update 1: Removed "requestAnimationFrame" portion, as I agree it's not necessary:

var listener = function() {
  // do whatever
};

slider1.addEventListener("input", function() {
  listener();
  slider1.addEventListener("change", listener);
});
slider1.addEventListener("change", function() {
  listener();
  slider1.removeEventListener("input", listener);
}); 

Update 2: Response to Andrew's 2nd Jun 2016 updated answer:

Thanks, Andrew - that appears to work in every browser I could find (Win desktop: IE, Chrome, Opera, FF; Android Chrome, Opera and FF, iOS Safari).

Update 3: if ("oninput in slider) solution

The following appears to work across all the above browsers. (I cannot find the original source now.) I was using this, but it subsequently failed on IE and so I went looking for a different one, hence I ended up here.

if ("oninput" in slider1) {
    slider1.addEventListener("input", function () {
        // do whatever;
    }, false);
}

But before I checked your solution, I noticed this was working again in IE - perhaps there was some other conflict.