60fps scrolling using pointer-events: none

Paul Lewis did an interesting article a while back about avoiding unnecessary paints through disabling hover effects as the user scrolls, which is a great approach. The down side being managing all your hover states through a parent class.

UPDATE: I’ve done a follow up article which demonstrates a more robust technique.

.hover .element:hover {
  box-shadow: 1px 1px 1px #000;
}

That approach doesn’t scale well and it creates unnecessary specificity in your CSS.

The gist of the technique is on scrolling the .hover class is removed from the body and all your .hover selectors won’t match until the user stops scrolling and the class is then added back to the body.

Then I saw a genius little tweet from Christian Schaefer.

Pointer-events property to the rescue

This is a much better approach as it will just make the mouse pass through the element that has the pointer-events: none property set. Take a look at the screencast I did showing the dramatic difference disabling hover makes.

We get all the benefits of the original approach without introducing maintainability and specificity issues in our CSS.

.disable-hover {
  pointer-events: none;
}

All we have to do is add the .disable-hover class to the body when the user begins to scroll. This then allows the users cursor to pass through the body and thus disable any hover effects.

var body = document.body,
    timer;

window.addEventListener('scroll', function() {
  clearTimeout(timer);
  if(!body.classList.contains('disable-hover')) {
    body.classList.add('disable-hover')
  }
  
  timer = setTimeout(function(){
    body.classList.remove('disable-hover')
  },500);
}, false);

The code is pretty simple we clear the timeout, this is important after the initial scroll, check to see if the disable class isn’t already on the body element and then set up a delayed timer to remove the class once the user has stopped scrolling for at least 500ms.

A more robust technique

Now applying pointer-events to the body will work just fine in most cases but if pointer-events: auto is applied to any child elements they will override the parent property and cause janky scroll.

.disable-hover,
.disable-hover * {
  pointer-events: none !important;
}

Easy solution is to do the asterisk selector and add an important to the property value so it will disable any child elements with pointer-events turned on.

Take a look at the testcase for yourself and run some timelines to see the performance gains from this simple technique.

Post filed under: css, javascript.

Skip to comment form.

  1. Ekrem Buyukkaya says:

    Brilliant.

  2. Chris says:

    Wonder if there are any side effects…
    BTW, the demo works nicely in IE11 too, Shows the same difference in the Performance tab in the new F12 Tools.

  3. Benz says:

    Jesuz so simple and so powerful – thx!

  4. Tyler says:

    Just a note, this won’t work with fullscreen applications that rely on content panels using `overflow: auto`, as `pointer-events: none` on the body element won’t register the scroll events. It appears the pointer-events property needs to be applied to a container within the scrollable area.

  5. Malte Ubl says:

    Simple, but doesn’t work as well as you think because this approach makes the document unclickable for up to 500ms after scrolling end. Here is an explanation how to fix it
    https://plus.google.com/+MalteUbl/posts/NsyYKenqYNP

  6. Chip says:

    Really nice – solves a specific issue we we’re having with hover events firing off while scrolling! The only issue I’m still seeing is the recalcute style (purple) event jumping to 30 fps each time the class is added to the body. I didn’t see the same thing in your video so I’m wondering if I’m doing something wrong.

    https://www.dropbox.com/s/jant444jwgo2jep/recalculate%20jump.png

  7. Ryan Seddon says:

    @Chip – Yep @derSchepp has said to set document.body.style directly rather than adding/removing a class to avoid a reflow. I’ll update the article real soon.

    Also I’d say it’s way less noticable in my video as my DOM structure is a lot simpler than your app you’re trying this in.

  8. Chip says:

    I tested Malte’s idea of adding / removing a cover div and that is working the best for me. Changing the document.body.style still causing a 30fps recalculate, but simply adding / removing a cover div with the following css worked perfect and keeps us at 60fps:

    .scroll-cover {
      top: 0;
      left: 0;
      bottom: 0;
      right: 0;
      position: fixed;
      pointer-events: auto !important;
      z-index: 10000;
    }
    
    var body = document.body,
        cover = document.createElement('div');
        cover.setAttribute('class','scroll-cover');
    
    window.addEventListener('scroll', function() {
      clearTimeout(timer);
      body.appendChild(cover);
      
      timer = setTimeout(function(){
        body.removeChild(cover);
      },500);
    }, false);
    
  9. Chip says:

    I was able to change the setTimeout to 100 to make clicks more responsive and still keep perf

  10. Matt Stuehler says:

    How does this apply to mobile devices (iOS in particular)? Is it at all relevant?

    And if it is… what are the implications of using -webkit-overflow-scrolling: touch? That seems to complicate the issue of knowing when the scroll event begins and ends…

  11. Peter says:

    This looks great! However there’s one thing I don’t understand – you disable pointer events and immediately after show that the hover states still work, before scrolling down the page at 60fps. How did the hover states still work? Were you enabling the JS version not just applying the CSS class? Thanks!

  12. sam says:

    Blown away. This is simply brilliant.

  13. Really clever sir.
    Thanks for sharing !

  14. Ryan Seddon says:

    @Peter –

    The hover effects are only disabled once you start to scroll so it won’t interfere with hover until then, so that’s why hover still works in the video after I click the disable button.

  15. Ryan Seddon says:

    @Matt – Hover on scroll isn’t really relevant to non mouse based devices as you can’t really trigger a hover. You should really avoid the use of :hover on touch based devices all together as it causes issues.

  16. Manumanu says:

    Great idea. Just wanted to say : IE11 naturally disable pointer events when scrolling. Dunno since which version, ’cause i wasn’t aware of this issue.

  17. Ryan Seddon says:

    @Manumanu – is that what they actually do? Do you have any info stating that’s what they do?

  18. Instead of document.body, consider applying this to the root element (document.documentElement). That way, it will still work if you use <html> and <body> as a free <div> — e.g. if you apply margin: 0 auto; max-width: 30em; box-shadow: …; to <body>.

  19. newable says:

    Great!

  20. It would be nice if this behaviour was the default in browsers.

  21. Hey Ryan,
    In my opinion, this is something browsers should be smart enough to do themselves. So it would make sense to me if one already did; IE11 may be? As @Manumanu suggests.

  22. JosiahS says:

    Interesting idea, but it seems to have the side effect of disabling multi-touch page scrolling on Chrome in Windows 8. (I haven’t tested it anywhere else.)

  23. Ron says:

    Any reason why this shouldn’t just be built into Chrome?

  24. RS says:

    Echoing poster above, yes brilliant indeed. I’ve got to say, one thing I personally find just annoying is hover effects on images–you know like those with an overlay and some icon–something, anything–showing that it’s a video post, or image post or whatever. But alas, every client wants this garbg! And of course when you’re scrolling down the page, and mouse pointer hits the images/elements then it can be a very unsatisfactory experience.

    Thanks for the post!

  25. Ryan Seddon says:

    @Josiah –

    Do you have any more details on this I don’t have access to a touch win8 machine with Chrome. Anyone else get this?

  26. Jake says:

    I tried this a while ago on a project and it does trigger a layout when pointer-events: none; is applied to the body (It does in your demo as well). I came up with a different solution but it was more complicated. It basically involves moving a 100x100px fixed position div under the mouse using translate3d() while scrolling, then moving it back out of the window when scrolling is done. That stops the hover effects and doesn’t trigger a layout. This was on a really complicated page, so that forced layout was more of an issue.

  27. Greg says:

    Thank you!!!

    I used this technique to solve a different problem which should also be worth mentioning.

    On my site I am using the Google Maps API and when I scroll down the page (using the scroll-wheel) the page suddenly stops once my mouse hovers over the map. The scroll-wheel would then start to re-scale the map which was annoying, I could disable the scroll wheel in the API but that would disable it permanently.

    Using this method the user can now scroll past the map using the scroll wheel AND re-scale the map if they so desire using the scroll wheel. Two worlds in harmony :)

  28. Ryan Seddon says:

    @Jake –

    That’s pretty interesting. I’m in the midst of doing a follow up article with all the different approaches that people have said and ficing existing issues some people have talked about. I’ll be investigating your approach as an alternative.

  29. Greg says:

    @Ryan

    I would be nice too if you included an example of this using JQuery as well. Perhaps using the .hasClass() attribute. It’s not necessary but would be appreciated :)

  30. Greg says:

    @Ryan

    Also not sure how this could be done, but it would awesome if the hover/click effect was triggered once you are done scrolling the page. Let me explain. If the user uses the scroll wheel to move up the page and ends up hovering over a button, the button’s hover effect (and click event) does not activate after 500ms, it only activates after 500ms has passed AND the user moves their cursor. It’s annoying because you have to wake the button up from it’s slumber by moving the cursor, rather than just waiting for the timer to run out.

  31. d43m says:

    Looks like it doesn’t change anything with firefox, at least on the demo site, the hover effect doesn’t trigger either way while scrolling.

    However, good tip for scrolling on the map Greg !

  32. Schalk Neethling says:

    Thanks for this awesome article! Here is how I ended up implementing it and it works great ~ https://gist.github.com/ossreleasefeed/7768761

  33. egiova says:

    Excellent little trick to avoid “light pollution”. What about side effects on touch screens?
    However, it remains to be tested at large scale. But good clue to follow…

  34. Ryan Seddon says:

    @Greg -

    I’m in the process of doing a follow up article and in that I have a solution to capture user clicks even when the user is still scrolling. Uses elem.elementFromPoint(x,y) plus triggering synthetic events on timeout fire.

  35. Ryan Seddon says:

    @d43m -

    Yes have noticed that not sure without the proper tools in their dev tools to see what is actually happening.

  36. Rob Erskine says:

    Rewrote this in jquery, you know, just in case:

    var body = $('body');
    var timer;
    
    $(window).on("scroll", function(){
        if(! body.hasClass('disable-hover')){
            body.addClass('disable-hover');
        }
    
        timer = setTimeout(function(){
            body.removeClass('disable-hover');
        },250);
    }, false);
    
  37. Greg Whitworth says:

    @Manumanu @RyanSeddon He is somewhat correct in regards to how IE11 handles :hover on scroll. To keep the explanation simple (we may post a blog post for more detail on the matter), we turn off hit testing if the mouse isn’t moving so you won’t get any un-necessary paints, however, there are some caveats to this for compatibility reasons. Even when using your suggested CSS+JS trick you won’t get too much perf increase in IE11 due to the fact that we use independent rendering (we’re rendering on a separate thread from the UI thread) but it would free up some CPU cycles for any script you might be running. Hope that helps!

  38. Ryan Seddon says:

    @Greg – Interesting, thanks for the insights!

  39. Yuanyan says:

    If u do it, it will make scrolling not work on mobile device.
    Test on mobile that “pointer-events: none” will disable the touch events. It’s seems not a good hack for mobile web.

  40. Alex says:

    Firefox also turns off hit testing while scrolling, and from a quick test of the test page this trick would probably slow it down (Changing the pointer-events causes it to repaint whatever the mouse was hovering over when the scroll started, which it doesn’t do otherwise)

Leave a comment