window.scrollTo with options not working on Microsoft Edge

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.


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.


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


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>