161

For a different question I composed this answer, including this sample code.

In that code I use the mouse wheel to zoom in/out of an HTML5 Canvas. I found some code that normalizes speed differences between Chrome and Firefox. However, the zoom handling in Safari is much, much faster than in either of those.

Here's the code I currently have:

var handleScroll = function(e){
  var delta = e.wheelDelta ? e.wheelDelta/40 : e.detail ? -e.detail/3 : 0;
  if (delta) ...
  return e.preventDefault() && false;
};
canvas.addEventListener('DOMMouseScroll',handleScroll,false); // For Firefox
canvas.addEventListener('mousewheel',handleScroll,false);     // Everyone else

What code can I use to get the same 'delta' value for the same amount of mouse wheel rolling across Chrome v10/11, Firefox v4, Safari v5, Opera v11 and IE9?

This question is related, but has no good answer.

Edit: Further investigation shows that one scroll event 'up' is:

                  | evt.wheelDelta | evt.detail
------------------+----------------+------------
  Safari v5/Win7  |       120      |      0
  Safari v5/OS X  |       120      |      0
  Safari v7/OS X  |        12      |      0
 Chrome v11/Win7  |       120      |      0
 Chrome v37/Win7  |       120      |      0
 Chrome v11/OS X  |         3 (!)  |      0      (possibly wrong)
 Chrome v37/OS X  |       120      |      0
        IE9/Win7  |       120      |  undefined
  Opera v11/OS X  |        40      |     -1
  Opera v24/OS X  |       120      |      0
  Opera v11/Win7  |       120      |     -3
 Firefox v4/Win7  |    undefined   |     -3
 Firefox v4/OS X  |    undefined   |     -1
Firefox v30/OS X  |    undefined   |     -1

Further, using the MacBook trackpad on OS X gives different results even when moving slowly:

  • On Safari and Chrome, the wheelDelta is a value of 3 instead of 120 for mouse wheel.
  • On Firefox the detail is usually 2, sometimes 1, but when scrolling very slowly NO EVENT HANDLER FIRES AT ALL.

So the question is:

What is the best way to differentiate this behavior (ideally without any user agent or OS sniffing)?

Community
  • 1
  • 1
Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • Sorry, I deleted my question. I'm writing up an answer right now. Before I get much further, are you talking about the scrolling on Safari on Mac OS X? When you scroll a little, it scrolls a little, but if you keep a constant rate, it progressively gets faster? – Blender Apr 03 '11 at 05:05
  • @Blender I am testing on OS X right now, and yes, Safari is the outlier which is zooming about 20x faster than Chrome. Unfortunately I don't have a physical mouse attached, so my testing is restricted to two-finger-swipes of ≈equivalent distances and speeds. – Phrogz Apr 03 '11 at 05:09
  • I've updated the question with details on the behavior of the top 5 browsers across OS X and Win7. It's a minefield, with Chrome on OS X appearing to be the problematic outlier. – Phrogz Apr 04 '11 at 16:39
  • @Phrogz Shouldn't it be `e.wheelDelta/120`? – Šime Vidas Apr 04 '11 at 17:26
  • @ŠimeVidas Yes, the code I copied and was using was clearly wrong. You can see better code in [my answer below](http://stackoverflow.com/questions/5527601/normalizing-mousewheel-speed-across-browsers/5542105#5542105). – Phrogz Apr 04 '11 at 17:30
  • @Phrogz I can confirm the results for Win7 ([Demo is here](http://jsfiddle.net/simevidas/E9c77/2/)) - `-3` in Firefox and `120` in all other browsers. The issue seems to be with OS X. You should try to get a confirmation from an OS X user (especially for Chrome). – Šime Vidas Apr 04 '11 at 17:34
  • Would be interesting to have this or another question addressing DOM3 `deltaX`, `deltaY` and `deltaZ` properties – Sergio Jun 19 '14 at 14:17
  • There should be a web standard for how often and when scroll/wheel events fire for web pages. – www139 Dec 09 '15 at 05:22
  • https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode is the key, see @George's answer – Charles L. Mar 01 '19 at 01:29

10 Answers10

59

Edit September 2014

Given that:

  • Different versions of the same browser on OS X have yielded different values in the past, and may do so in the future, and that
  • Using the trackpad on OS X yields very similar effects to using a mouse wheel, yet gives very different event values, and yet the device difference cannot be detected by JS

…I can only recommend using this simple, sign-based-counting code:

var handleScroll = function(evt){
  if (!evt) evt = event;
  var direction = (evt.detail<0 || evt.wheelDelta>0) ? 1 : -1;
  // Use the value as you will
};
someEl.addEventListener('DOMMouseScroll',handleScroll,false); // for Firefox
someEl.addEventListener('mousewheel',    handleScroll,false); // for everyone else

Original attempt to be correct follows.

Here is my first attempt at a script to normalize the values. It has two flaws on OS X: Firefox on OS X will produce values 1/3 what they should be, and Chrome on OS X will produce values 1/40 what they should be.

// Returns +1 for a single wheel roll 'up', -1 for a single roll 'down'
var wheelDistance = function(evt){
  if (!evt) evt = event;
  var w=evt.wheelDelta, d=evt.detail;
  if (d){
    if (w) return w/d/40*d>0?1:-1; // Opera
    else return -d/3;              // Firefox;         TODO: do not /3 for OS X
  } else return w/120;             // IE/Safari/Chrome TODO: /3 for Chrome OS X
};

You can test out this code on your own browser here: http://phrogz.net/JS/wheeldelta.html

Suggestions for detecting and improving the behavior on Firefox and Chrome on OS X are welcome.

Edit: One suggestion from @Tom is to simply count each event call as a single move, using the sign of the distance to adjust it. This will not give great results under smooth/accelerated scrolling on OS X, nor handle perfectly cases when the mouse wheel is moved very fast (e.g. wheelDelta is 240), but these happen infrequently. This code is now the recommended technique shown at the top of this answer, for the reasons described there.

Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • @ŠimeVidas Thanks, that's basically what I have, except that I also account for the 1/3 difference on Opera OS X. – Phrogz Apr 04 '11 at 17:55
  • @Phrogz, do you have an updated version in Sept' 2014 with all OS X /3 added ? This would be a great addition for community! – Basj Sep 24 '14 at 22:04
  • @Phrogz, this would be great. I don't have a Mac here to test... (I would be happy to give a bounty for that, even if I don't have much reputation myself ;)) – Basj Sep 25 '14 at 07:41
  • @Basj I have updated the test results in the original question. Given that (a) Safari on OS X now yields `12` instead of `120`, while all other browsers on OS X have gotten `better`, and (b) there is no way to differentiate events coming from the trackpad versus a mouse, yet they give drastically different event details, I am editing my answer above to recommend the simple `wheelDirection` test. – Phrogz Sep 25 '14 at 12:31
  • thanks @Phrogz for having investigated on what happens in OS X. The problem is that if we only use the "sign-based counting" method, it will give very different results : try my tool http://www.bigpicture.bi/demo (which uses basic counting like the one you suggest) : it works on FF, Chrome, IE (Win7). But it seems that zooming is far too fast with OS X... what do you think ? – Basj Sep 26 '14 at 12:50
  • @Basj Yes, with the track pad you get many many events on OS X. Only other hack I can think of is to wrap the content in an infinitely-scrolling container, make the content position-fixed, and then detect the scrollOffset changes of the container. – Phrogz Sep 26 '14 at 14:38
  • @Flek Your edit was incorrect, and drastically changed the answer. Please post a comment for such a drastic edit in the future. Note, from the "investigation" posted in the question, the `wheelDelta` and `detail` are opposite signs. The `evt.detail` test is necessary for FireFox. – Phrogz Jan 13 '15 at 17:18
  • @Phrogz Oh I've mixed something up with my own scripts. Sorry for the confusion. – F Lekschas Jan 13 '15 at 17:53
  • 1
    On windows Firefox 35.0.1, wheelDelta is undefined and detail is always 0, which makes the supplied code fail. – Max Strater Feb 14 '15 at 03:53
  • 1
    @MaxStrater Faced the same problem, I've added "deltaY" to overcome this in direction like that `(((evt.deltaY <0 || evt.wheelDelta>0) || evt.deltaY < 0) ? 1 : -1)` not sure what QA finds out with that, though. – Brock Mar 27 '15 at 09:39
  • The best and most comprehensive solution is to use this code from Facebook http://stackoverflow.com/a/30134826/4387229 – Simon Watson Oct 29 '16 at 11:09
37

Our friends at Facebook put together a great solution to this problem.

I have tested on a data table that I'm building using React and it scrolls like butter!

This solution works on a variety of browsers, on Windows/Mac, and both using trackpad/mouse.

// Reasonable defaults
var PIXEL_STEP  = 10;
var LINE_HEIGHT = 40;
var PAGE_HEIGHT = 800;

function normalizeWheel(/*object*/ event) /*object*/ {
  var sX = 0, sY = 0,       // spinX, spinY
      pX = 0, pY = 0;       // pixelX, pixelY

  // Legacy
  if ('detail'      in event) { sY = event.detail; }
  if ('wheelDelta'  in event) { sY = -event.wheelDelta / 120; }
  if ('wheelDeltaY' in event) { sY = -event.wheelDeltaY / 120; }
  if ('wheelDeltaX' in event) { sX = -event.wheelDeltaX / 120; }

  // side scrolling on FF with DOMMouseScroll
  if ( 'axis' in event && event.axis === event.HORIZONTAL_AXIS ) {
    sX = sY;
    sY = 0;
  }

  pX = sX * PIXEL_STEP;
  pY = sY * PIXEL_STEP;

  if ('deltaY' in event) { pY = event.deltaY; }
  if ('deltaX' in event) { pX = event.deltaX; }

  if ((pX || pY) && event.deltaMode) {
    if (event.deltaMode == 1) {          // delta in LINE units
      pX *= LINE_HEIGHT;
      pY *= LINE_HEIGHT;
    } else {                             // delta in PAGE units
      pX *= PAGE_HEIGHT;
      pY *= PAGE_HEIGHT;
    }
  }

  // Fall-back if spin cannot be determined
  if (pX && !sX) { sX = (pX < 1) ? -1 : 1; }
  if (pY && !sY) { sY = (pY < 1) ? -1 : 1; }

  return { spinX  : sX,
           spinY  : sY,
           pixelX : pX,
           pixelY : pY };
}

The source code can be found here: https://github.com/facebook/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js

George
  • 589
  • 5
  • 6
  • 3
    A more direct link that has not been bundled to the original code for normalizeWHeel.js https://github.com/facebook/fixed-data-table/blob/master/src/vendor_upstream/dom/normalizeWheel.js – Robin Luiten Jan 30 '16 at 00:48
  • Thanks @RobinLuiten, updating original post. – George Feb 01 '16 at 01:42
  • This stuff is brilliant. Just made use of it and works like a charm! Good job Facebook :) – perry Apr 21 '16 at 00:27
  • Could you give some example of how to use it? I tried it and it works in FF but not in Chrome or IE (11)..? Thanks – Andrew Oct 28 '16 at 15:56
  • 5
    For anyone using npm there's a ready to use package of just this code already extracted from Facebook's Fixed Data Table. See here for more details https://www.npmjs.com/package/normalize-wheel – Simon Watson Oct 29 '16 at 11:04
  • Could you enlighten why would it work? I understand the calculation and all. But not quite sure how to tie this into event handling loop. This function (as far as I can tell), simply returns an object. How does the browsers know to use this object to determine its scrolling distance? – yiwen Feb 26 '19 at 23:33
  • This works because of https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode where deltaMode of 1 is multiplied by 40 and delta mode of 2 is multiplied by 800. 40 and 800 I believe were derived empirically – Charles L. Mar 01 '19 at 01:28
  • 2
    I use this for a zoom control, but for me there is a huge difference between using the Macbook trackpad and using my Logitech MX Anywhere 2 mouse with this code. – Dirk Boer Apr 07 '20 at 21:49
28

Here is my crazy attempt to produce a cross browser coherent and normalized delta ( -1 <= delta <= 1 ) :

var o = e.originalEvent,
    d = o.detail, w = o.wheelDelta,
    n = 225, n1 = n-1;

// Normalize delta
d = d ? w && (f = w/d) ? d/f : -d/1.35 : w/120;
// Quadratic scale if |d| > 1
d = d < 1 ? d < -1 ? (-Math.pow(d, 2) - n1) / n : d : (Math.pow(d, 2) + n1) / n;
// Delta *should* not be greater than 2...
e.delta = Math.min(Math.max(d / 2, -1), 1);

This is totally empirical but works quite good on Safari 6, FF 16, Opera 12 (OS X) and IE 7 on XP

smrtl
  • 604
  • 5
  • 7
12

I made a table with different values returned by different events/browsers, taking into account the DOM3 wheel event that some browsers already support (table under).

Based on that I made this function to normalize the speed:

http://jsfiddle.net/mfe8J/1/

function normalizeWheelSpeed(event) {
    var normalized;
    if (event.wheelDelta) {
        normalized = (event.wheelDelta % 120 - 0) == -0 ? event.wheelDelta / 120 : event.wheelDelta / 12;
    } else {
        var rawAmmount = event.deltaY ? event.deltaY : event.detail;
        normalized = -(rawAmmount % 3 ? rawAmmount * 10 : rawAmmount / 3);
    }
    return normalized;
}

Table for mousewheel, wheel and DOMMouseScroll events:

| mousewheel        | Chrome (win) | Chrome (mac) | Firefox (win) | Firefox (mac) | Safari 7 (mac) | Opera 22 (mac) | Opera 22 (win) | IE11      | IE 9 & 10   | IE 7 & 8  |
|-------------------|--------------|--------------|---------------|---------------|----------------|----------------|----------------|-----------|-------------|-----------|
| event.detail      | 0            | 0            | -             | -             | 0              | 0              | 0              | 0         | 0           | undefined |
| event.wheelDelta  | 120          | 120          | -             | -             | 12             | 120            | 120            | 120       | 120         | 120       |
| event.wheelDeltaY | 120          | 120          | -             | -             | 12             | 120            | 120            | undefined | undefined   | undefined |
| event.wheelDeltaX | 0            | 0            | -             | -             | 0              | 0              | 0              | undefined | undefined   | undefined |
| event.delta       | undefined    | undefined    | -             | -             | undefined      | undefined      | undefined      | undefined | undefined   | undefined |
| event.deltaY      | -100         | -4           | -             | -             | undefined      | -4             | -100           | undefined | undefined   | undefined |
| event.deltaX      | 0            | 0            | -             | -             | undefined      | 0              | 0              | undefined | undefined   | undefined |
|                   |              |              |               |               |                |                |                |           |             |           |
| wheel             | Chrome (win) | Chrome (mac) | Firefox (win) | Firefox (mac) | Safari 7 (mac) | Opera 22 (mac) | Opera 22 (win) | IE11      | IE 10 & 9   | IE 7 & 8  |
| event.detail      | 0            | 0            | 0             | 0             | -              | 0              | 0              | 0         | 0           | -         |
| event.wheelDelta  | 120          | 120          | undefined     | undefined     | -              | 120            | 120            | undefined | undefined   | -         |
| event.wheelDeltaY | 120          | 120          | undefined     | undefined     | -              | 120            | 120            | undefined | undefined   | -         |
| event.wheelDeltaX | 0            | 0            | undefined     | undefined     | -              | 0              | 0              | undefined | undefined   | -         |
| event.delta       | undefined    | undefined    | undefined     | undefined     | -              | undefined      | undefined      | undefined | undefined   | -         |
| event.deltaY      | -100         | -4           | -3            | -0,1          | -              | -4             | -100           | -99,56    | -68,4 | -53 | -         |
| event.deltaX      | 0            | 0            | 0             | 0             | -              | 0              | 0              | 0         | 0           | -         |
|                   |              |              |               |               |                |                |                |           |             |           |
|                   |              |              |               |               |                |                |                |           |             |           |
| DOMMouseScroll    |              |              | Firefox (win) | Firefox (mac) |                |                |                |           |             |           |
| event.detail      |              |              | -3            | -1            |                |                |                |           |             |           |
| event.wheelDelta  |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.wheelDeltaY |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.wheelDeltaX |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.delta       |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.deltaY      |              |              | undefined     | undefined     |                |                |                |           |             |           |
| event.deltaX      |              |              | undefined     | undefined     |                |                |                |           |             |           |
Community
  • 1
  • 1
Sergio
  • 28,539
  • 11
  • 85
  • 132
6

Another more or less self-contained solution...

This doesn't take time between events into account though. Some browsers seem to always fire events with the same delta, and just fire them faster when scrolling quickly. Others do vary the deltas. One can imagine an adaptive normalizer that takes time into account, but that'd get somewhat involved and awkward to use.

Working available here: jsbin/iqafek/2

var normalizeWheelDelta = function() {
  // Keep a distribution of observed values, and scale by the
  // 33rd percentile.
  var distribution = [], done = null, scale = 30;
  return function(n) {
    // Zeroes don't count.
    if (n == 0) return n;
    // After 500 samples, we stop sampling and keep current factor.
    if (done != null) return n * done;
    var abs = Math.abs(n);
    // Insert value (sorted in ascending order).
    outer: do { // Just used for break goto
      for (var i = 0; i < distribution.length; ++i) {
        if (abs <= distribution[i]) {
          distribution.splice(i, 0, abs);
          break outer;
        }
      }
      distribution.push(abs);
    } while (false);
    // Factor is scale divided by 33rd percentile.
    var factor = scale / distribution[Math.floor(distribution.length / 3)];
    if (distribution.length == 500) done = factor;
    return n * factor;
  };
}();

// Usual boilerplate scroll-wheel incompatibility plaster.

var div = document.getElementById("thing");
div.addEventListener("DOMMouseScroll", grabScroll, false);
div.addEventListener("mousewheel", grabScroll, false);

function grabScroll(e) {
  var dx = -(e.wheelDeltaX || 0), dy = -(e.wheelDeltaY || e.wheelDelta || 0);
  if (e.detail != null) {
    if (e.axis == e.HORIZONTAL_AXIS) dx = e.detail;
    else if (e.axis == e.VERTICAL_AXIS) dy = e.detail;
  }
  if (dx) {
    var ndx = Math.round(normalizeWheelDelta(dx));
    if (!ndx) ndx = dx > 0 ? 1 : -1;
    div.scrollLeft += ndx;
  }
  if (dy) {
    var ndy = Math.round(normalizeWheelDelta(dy));
    if (!ndy) ndy = dy > 0 ? 1 : -1;
    div.scrollTop += ndy;
  }
  if (dx || dy) { e.preventDefault(); e.stopPropagation(); }
}
Billy Moon
  • 57,113
  • 24
  • 136
  • 237
Marijn
  • 8,691
  • 2
  • 34
  • 37
4

Simple and working solution:

private normalizeDelta(wheelEvent: WheelEvent):number {
    var delta = 0;
    var wheelDelta = wheelEvent.wheelDelta;
    var deltaY = wheelEvent.deltaY;
    // CHROME WIN/MAC | SAFARI 7 MAC | OPERA WIN/MAC | EDGE
    if (wheelDelta) {
        delta = -wheelDelta / 120; 
    }
    // FIREFOX WIN / MAC | IE
    if(deltaY) {
        deltaY > 0 ? delta = 1 : delta = -1;
    }
    return delta;
}
Marek
  • 3,935
  • 10
  • 46
  • 70
3

For zoom support on touch devices, register for the gesturestart, gesturechange and gestureend events and use the event.scale property. You can see example code for this.

For Firefox 17 the onwheel event is planned to be supported by desktop and mobile versions (as per MDN docs on onwheel). Also for Firefox maybe the Gecko specific MozMousePixelScroll event is useful (although presumably this is now deprecated since the DOMMouseWheel event is now deprecated in Firefox).

For Windows, the driver itself seems to generate the WM_MOUSEWHEEL, WM_MOUSEHWHEEL events (and maybe the WM_GESTURE event for touchpad panning?). That would explain why Windows or the browser doesn't seem to normalise the mousewheel event values itself (and might mean you cannot write reliable code to normalise the values).

For onwheel (not onmousewheel) event support in Internet Explorer for IE9 and IE10, you can also use the W3C standard onwheel event. However one notch can be a value different from 120 (e.g. a single notch becomes 111 (instead of -120) on my mouse using this test page). I wrote another article with other details wheel events that might be relevant.

Basically in my own testing for wheel events (I am trying to normalise the values for scrolling), I have found that I get varying values for OS, browser vendor, browser version, event type, and device (Microsoft tiltwheel mouse, laptop touchpad gestures, laptop touchpad with scrollzone, Apple magic mouse, Apple mighty mouse scrollball, Mac touchpad, etc etc).

And have to ignore a variety of side-effects from browser configuration (e.g. Firefox mousewheel.enable_pixel_scrolling, chrome --scroll-pixels=150), driver settings (e.g. Synaptics touchpad), and OS configuration (Windows mouse settings, OSX Mouse preferences, X.org button settings).

Community
  • 1
  • 1
robocat
  • 5,293
  • 48
  • 65
3

This is a problem I've been fighting with for some hours today, and not for the first time :(

I've been trying to sum up values over a "swipe" and see how different browsers report values, and they vary a lot, with Safari reporting order of magnitude bigger numbers on almost all platforms, Chrome reporting quite more (like 3 times more) than firefox, firefox being balanced on the long run but quite different among platforms on small movements (on Ubuntu gnome, nearly only +3 or -3, seems like it sums up smaller events and then send a big "+3")

The current solutions found right now are three :

  1. The already mentioned "use only the sign" which kills any kind of acceleration
  2. Sniff the browser up to minor version and platform, and adjust properly
  3. Qooxdoo recently implemented a self adapting algorithm, which basically tries to scale the delta based on minimum and maximum value received so far.

The idea in Qooxdoo is good, and works, and is the only solution I've currently found to be completely consistent cross browser.

Unfortunately it tends to renormalize also the acceleration. If you try it (in their demos), and scroll up and down at maximum speed for a while, you'll notice that scrolling extremely fast or extremely slow basically produce nearly the same amount of movement. On the opposite if you reload the page and only swipe very slowly, you'll notice that it will scroll quite fast".

This is frustrating for a Mac user (like me) used to give vigorous scroll swipes on the touchpad and expecting to get to the top or bottom of the scrolled thing.

Even more, since it scales down the mouse speed based on the maximum value obtained, the more your user tries to speed it up, the more it will slow down, while a "slow scrolling" user will experience quite fast speeds.

This makes this (otherwise brilliant) solution a slightly better implementation of solution 1.

I ported the solution to the jquery mousewheel plugin : http://jsfiddle.net/SimoneGianni/pXzVv/

If you play with it for a while, You'll see that you'll start getting quite homogeneous results, but you'll also notice that it tend to +1/-1 values quite fast.

I'm now working on enhancing it to detect peaks better, so that they don't send everything "out of scale". It would also be nice to also obtain a float value between 0 and 1 as the delta value, so that there is a coherent output.

Simone Gianni
  • 11,426
  • 40
  • 49
1

There is definitely no simple way to normalize across all users in all OS in all browsers.

It gets worse than your listed variations - on my WindowsXP+Firefox3.6 setup my mousewheel does 6 per one-notch scroll - probably because somewhere I've forgotten I've accelerated the mouse wheel, either in the OS or somewhere in about:config

However I am working on a similar problem (with a similar app btw, but non-canvas) and it occurs to me by just using the delta sign of +1 / -1 and measuring over time the last time it fired, you'll have a rate of acceleration, ie. if someone scrolls once vs several times in a few moments (which I would bet is how google maps does it).

The concept seems to work well in my tests, just make anything less than 100ms add to the acceleration.

ck_
  • 3,353
  • 5
  • 31
  • 33
-2
var onMouseWheel = function(e) {
    e = e.originalEvent;
    var delta = e.wheelDelta>0||e.detail<0?1:-1;
    alert(delta);
}
$("body").bind("mousewheel DOMMouseScroll", onMouseWheel);