16

I have a strange issue which I can only replicate on Microsoft browsers (Edge and IE11 tested).

<style>
    body {
        height: 5000px;
        width: 5000px;
    }
</style>
<p>Click the button to scroll the document window to 1000 pixels.</p>
<button onclick="scrollWin()">Click me to scroll!</button>
<script>
    function scrollWin() {
        window.scrollTo({
            left: 1000, 
            top: 1000,
            behavior:"smooth"
        });
    }
</script>

This code correctly scrolls the window 1000px to the left and down, with a smooth behaviour in Chrome and Firefox. However, on Edge and IE, it does not move at all.

CDK
  • 457
  • 1
  • 4
  • 17
  • `window.scrollTo(1000,1000);` is the original signatire. I assume Edge and IE did not change that but I might be wrong since MDN says they implemented this – mplungjan Sep 11 '18 at 12:46
  • according to MDN, the `(options)` variant has always existed in all browsers – Jaromanda X Sep 11 '18 at 12:50
  • Known bug: https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/15534521/ – mplungjan Sep 11 '18 at 12:54
  • @mplungjan - you are correct, and if I just use `window.scrollTo(1000,1000);` it does scroll (obviously not smoothly) - but as @Jaromanda X says, MDN suggests the options should be supported by all browsers: https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo – CDK Sep 11 '18 at 12:55
  • @JaromandaX that is incorrect. It is for sure a new variant - I cannot get scrollTo(1000,1000) to work in Edge either – mplungjan Sep 11 '18 at 12:56
  • Here I even get an error in Edge: http://jsfiddle.net/mplungjan/u3xyp41r/ when scrolling a div – mplungjan Sep 11 '18 at 13:01

7 Answers7

21

As mentioned before, the Scroll Behavior specification has only been implemented in Chrome, Firefox and Opera.

Here's a one-liner to detect support for the behavior property in ScrollOptions:

const supportsNativeSmoothScroll = 'scrollBehavior' in document.documentElement.style;

And here's a simple implementation for cross-browser smooth scrolling: https://gist.github.com/eyecatchup/d210786daa23fd57db59634dd231f341

eyecatchUp
  • 8,385
  • 4
  • 48
  • 61
  • 1
    that's testing for support of the CSS property `scrollBehavior`. What we want to test is the support for the DOM API. What if someday CSSOM remove this property but DOM keeps that API? Your code will break. – Kaiido Jul 26 '19 at 06:52
13

Maybe not a true answer in the sense of the word, but I have solved this problem by using this helpful polyfill: https://github.com/iamdustan/smoothscroll which works really well across all browsers.

Example page for pollyfill: http://iamdustan.com/smoothscroll/

Many thanks to the author.

CDK
  • 457
  • 1
  • 4
  • 17
  • 1
    this "smoothscroll" polyfill supports only "smooth" option. To support all options in scrollIntoViewOptions it's better to use https://www.npmjs.com/package/seamless-scroll-polyfill – Julia Oct 09 '20 at 10:27
  • no longer works – oldboy Dec 30 '20 at 02:32
  • It looks like the entire issue is no longer present in the Chromium version (I am looking at version 87) – CDK Jan 05 '21 at 10:07
7

You can detect support for the behavior option in scrollTo using this snippet:

function testSupportsSmoothScroll () {
  var supports = false
  try {
    var div = document.createElement('div')
    div.scrollTo({
      top: 0,
      get behavior () {
        supports = true
        return 'smooth'
      }
    })
  } catch (err) {}
  return supports
}

Tested in Chrome, Firefox, Safari, and Edge, and seems to work correctly. If supports is false, you fall back to a polyfill.

nlawson
  • 11,050
  • 3
  • 35
  • 47
  • This question makes me feel more and more stupid ;-). Great idea, I don't know how I could miss it, when I use the same every day for testing addEventListener's options... – Kaiido Jan 25 '19 at 08:44
  • 1
    Wouldn't be `const supportsNativeSmoothScroll = 'scrollBehavior' in document.documentElement.style;` much easier? _(Tested in Chrome, Firefox, Safari, and Edge, and seems to work correctly.)_ – eyecatchUp Mar 18 '19 at 11:42
  • @eyecatchUp but that's testing for support of the CSS property `scrollBehavior`. What we want to test is the support for the DOM API. What if someday CSSOM remove this property but DOM keeps that API? Your code will break. – Kaiido Jul 26 '19 at 06:51
3

Indeed, they don't support this variant, MDN articles should be updated.

One way to polyfill this method is to run the scroll method in a requestAnimationFrame powered loop. Nothing too fancy here.

The main problem that arises is how to detect when this variant is not supported. actually @nlawson's answer tackles this problem perfectly...

For this, we can use the fact that a call to Window#scroll will fire a ScrollEvent if the viewPort actually did scroll.
This means we can set up an asynchronous test that will:

  1. Attach an event handler to the ScrollEvent,
  2. Call a first time scroll(left , top) variant to be sure the Event will fire,
  3. Overwrite this call with a second one using the options variant.
  4. In the event handler, if we aren't at the correct scroll position, this means we need to attach our polyfill.

So the caveat of this test is that it is an asynchronous test. But since you need to actually wait for the document has loaded before calling this method, I guess in 99% of cases it will be ok.

Now to less burden the main doc, and since it is already an asynchronous test, we can even wrap this test inside an iframe, which gives us something like:

/* Polyfills the Window#scroll(options) & Window#scrollTo(options) */
(function ScrollPolyfill() {

  // The asynchronous tester

  // wrapped in an iframe (will not work in SO's StackSnippet®)
  var iframe = document.createElement('iframe');
  iframe.onload = function() {
    var win = iframe.contentWindow;
    // listen for a scroll event
    win.addEventListener('scroll', function handler(e){
      // when the scroll event fires, check that we did move
      if(win.pageXOffset < 99) { // !== 0 should be enough, but better be safe
        attachPolyfill();
      }
      // cleanup
      document.body.removeChild(iframe);      
    });
    // set up our document so we can scroll
    var body = win.document.body;
    body.style.width = body.style.height = '1000px';

    win.scrollTo(10, 0); // force the event
    win.scrollTo({left:100, behavior:'instant'}); // the one we actually test
  };
  // prepare our frame
  iframe.src = "about:blank";
  iframe.setAttribute('width', 1);
  iframe.setAttribute('height', 1);
  iframe.setAttribute('style', 'position:absolute;z-index:-1');
  iframe.onerror = function() {
    console.error('failed to load the frame, try in jsfiddle');
  };
  document.body.appendChild(iframe);

  // The Polyfill

  function attachPolyfill() {
    var original = window.scroll, // keep the original method around
      animating = false, // will keep our timer's id
      dx = 0,
      dy = 0,
      target = null;

    // override our methods
    window.scrollTo = window.scroll = function polyfilledScroll(user_opts) {
      // if we are already smooth scrolling, we need to stop the previous one
      // whatever the current arguments are
      if(animating) {
        clearAnimationFrame(animating);
      }

      // not the object syntax, use the default
      if(arguments.length === 2) {
        return original.apply(this, arguments);
      }
      if(!user_opts || typeof user_opts !== 'object') {
        throw new TypeError("value can't be converted to a dictionnary");
      }

      // create a clone to not mess the passed object
      // and set missing entries
      var opts = {
        left: ('left' in user_opts) ? user_opts.left : window.pageXOffset,
        top:  ('top' in user_opts) ? user_opts.top : window.pageYOffset,
        behavior: ('behavior' in user_opts) ? user_opts.behavior : 'auto',
      };
      if(opts.behavior !== 'instant' && opts.behavior !== 'smooth') {
        // parse 'auto' based on CSS computed value of 'smooth-behavior' property
        // But note that if the browser doesn't support this variant
        // There are good chances it doesn't support the CSS property either...
        opts.behavior = window.getComputedStyle(document.scrollingElement || document.body)
            .getPropertyValue('scroll-behavior') === 'smooth' ?
                'smooth' : 'instant';
      }
      if(opts.behavior === 'instant') {
        // not smooth, just default to the original after parsing the oject
        return original.call(this, opts.left, opts.top);
      }

      // update our direction
      dx = (opts.left - window.pageXOffset) || 0;
      dy = (opts.top - window.pageYOffset) || 0;

      // going nowhere
      if(!dx && !dy) {
        return;
      }
      // save passed arguments
      target = opts;
      // save the rAF id
      animating = anim();

    };
    // the animation loop
    function anim() {
      var freq = 16 / 300, // whole anim duration is approximately 300ms @60fps
        posX, poxY;
      if( // we already reached our goal on this axis ?
        (dx <= 0 && window.pageXOffset <= +target.left) ||
        (dx >= 0 && window.pageXOffset >= +target.left) 
      ){
        posX = +target.left;
      }
      else {
        posX = window.pageXOffset + (dx * freq);
      }

      if(
        (dy <= 0 && window.pageYOffset <= +target.top) ||
        (dy >= 0 && window.pageYOffset >= +target.top) 
      ){
        posY = +target.top;
      }
      else {
        posY = window.pageYOffset + (dx * freq);
      }
      // move to the new position
      original.call(window, posX, posY);
      // while we are not ok on both axis
      if(posX !== +target.left || posY !== +target.top) {
        requestAnimationFrame(anim);
      }
      else {
        animating = false;
      }
    }
  }
})();


Sorry for not providing a runable demo inside the answer directly, but StackSnippet®'s over-protected iframes don't allow us to access the content of an inner iframe on IE...
So instead, here is a link to a jsfiddle.


Post-scriptum: Now comes to my mind that it might actually be possible to check for support in a synchronous way by checking for the CSS scroll-behavior support, but I'm not sure it really covers all UAs in the history...


Post-Post-scriptum: Using @nlawson's detection we can now have a working snippet ;-)

/* Polyfills the Window#scroll(options) & Window#scrollTo(options) */
(function ScrollPolyfill() {

  // The synchronous tester from @nlawson's answer
  var supports = false
    test_el = document.createElement('div'),
    test_opts = {top:0};
  // ES5 style for IE
  Object.defineProperty(test_opts, 'behavior', {
    get: function() {
      supports = true;
    }
  });
  try {
    test_el.scrollTo(test_opts);
  }catch(e){};
  
  if(!supports) {
    attachPolyfill();
  }

  function attachPolyfill() {
    var original = window.scroll, // keep the original method around
      animating = false, // will keep our timer's id
      dx = 0,
      dy = 0,
      target = null;

    // override our methods
    window.scrollTo = window.scroll = function polyfilledScroll(user_opts) {
      // if we are already smooth scrolling, we need to stop the previous one
      // whatever the current arguments are
      if(animating) {
        clearAnimationFrame(animating);
      }

      // not the object syntax, use the default
      if(arguments.length === 2) {
        return original.apply(this, arguments);
      }
      if(!user_opts || typeof user_opts !== 'object') {
        throw new TypeError("value can't be converted to a dictionnary");
      }

      // create a clone to not mess the passed object
      // and set missing entries
      var opts = {
        left: ('left' in user_opts) ? user_opts.left : window.pageXOffset,
        top:  ('top' in user_opts) ? user_opts.top : window.pageYOffset,
        behavior: ('behavior' in user_opts) ? user_opts.behavior : 'auto',
      };
    if(opts.behavior !== 'instant' && opts.behavior !== 'smooth') {
      // parse 'auto' based on CSS computed value of 'smooth-behavior' property
        // But note that if the browser doesn't support this variant
        // There are good chances it doesn't support the CSS property either...
      opts.behavior = window.getComputedStyle(document.scrollingElement || document.body)
        .getPropertyValue('scroll-behavior') === 'smooth' ?
          'smooth' : 'instant';
    }
    if(opts.behavior === 'instant') {
        // not smooth, just default to the original after parsing the oject
        return original.call(this, opts.left, opts.top);
      }

      // update our direction
      dx = (opts.left - window.pageXOffset) || 0;
      dy = (opts.top - window.pageYOffset) || 0;

      // going nowhere
      if(!dx && !dy) {
        return;
      }
      // save passed arguments
      target = opts;
      // save the rAF id
      animating = anim();

    };
    // the animation loop
    function anim() {
      var freq = 16 / 300, // whole anim duration is approximately 300ms @60fps
        posX, poxY;
      if( // we already reached our goal on this axis ?
        (dx <= 0 && window.pageXOffset <= +target.left) ||
        (dx >= 0 && window.pageXOffset >= +target.left) 
      ){
        posX = +target.left;
      }
      else {
        posX = window.pageXOffset + (dx * freq);
      }

      if(
        (dy <= 0 && window.pageYOffset <= +target.top) ||
        (dy >= 0 && window.pageYOffset >= +target.top) 
      ){
        posY = +target.top;
      }
      else {
        posY = window.pageYOffset + (dx * freq);
      }
      // move to the new position
      original.call(window, posX, posY);
      // while we are not ok on both axis
      if(posX !== +target.left || posY !== +target.top) {
        requestAnimationFrame(anim);
      }
      else {
        animating = false;
      }
    }
  }
})();

// OP's code,
// by the time you click the button, the polyfill should already be set up if needed
function scrollWin() {
  window.scrollTo({
    left: 1000,
    top: 1000,
    behavior: 'smooth'
  });
}
body {
  height: 5000px;
  width: 5000px;
}
<p>Click the button to scroll the document window to 1000 pixels.</p>
<button onclick="scrollWin()">Click me to scroll!</button>
Kaiido
  • 87,051
  • 7
  • 143
  • 194
  • Wow, this is an in depth answer for creating a polyfill from scratch. I checked it and it works well, but will stick with the simpler polyfill that avoids iframes linked from my own answer. Thanks for providing this though! – CDK Sep 14 '18 at 12:05
  • 1
    @CDK han.... of course someone already did it, bettter... Feel a bit stupid now to have wrote this in the first place without I saw your answer... Note that you can accept your own answer, which is not a bad one. And I see they used the idea I had in my Ps for detection, so it might be reliable enough after all... – Kaiido Sep 14 '18 at 12:26
0

Unfortunately, there is no way for that method to work in these two browsers. You can check open issues here and see that they have done nothing on this issue. https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/15534521/

Lazar Nikolic
  • 2,798
  • 1
  • 14
  • 29
0

You can try to use Element.ScrollLeft and Element.ScrollTop property with Window.scrollTo().

Below is the example which is working with Edge and other browsers.

<html>
<style>
    body {
        height: 5000px;
        width: 5000px;
    }
</style>
<p>Click the button to scroll the document window to 1000 pixels.</p>
<button onclick="scrollWin(this)">Click me to scroll!</button>
<script>
    function scrollWin(pos) {
        window.scrollTo(pos.offsetTop+1000,pos.offsetLeft+1000);
            
      
    }
</script>
</html>

Smooth behavior is not working with this code.

Reference:

Element.scrollLeft

Element.scrollTop

Regards

Deepak

Deepak-MSFT
  • 8,111
  • 1
  • 6
  • 14
  • Thanks @deepak - but the main reason I wanted to use scrollTo with the options was for the smooth scroll affect. Appreciate your answer though. – CDK Sep 13 '18 at 15:13
  • @ CDK, I again made a several tests with smooth behavior with Edge. But this behavior is not working. so at present, It looks like this behavior is not supported with Edge. – Deepak-MSFT Sep 14 '18 at 01:46
0

The "smoothscroll" polyfill supports only "smooth" option. To support all options in scrollIntoViewOptions it's better to use seamless-scroll-polyfill (https://www.npmjs.com/package/seamless-scroll-polyfill)

Worked for me.

Here is a link with explanation https://github.com/Financial-Times/polyfill-library/issues/657

Julia
  • 484
  • 4
  • 14