Javascript - window.scroll({ behavior: 'smooth' }) not working in Safari

As the title says, it works perfectly fine on Chrome. But in Safari, it just sets the page to the desired top and and left position. Is this an expected behaviour? Is there a way to make it work nicely?

Answers:

Answer

Behavior options aren't fully supported in IE/Edge/Safari, so you'd have to implement something on your own. I believe jQuery has something already, but if you're not using jQuery, here's a pure JavaScript implementation:

function SmoothVerticalScrolling(e, time, where) {
    var eTop = e.getBoundingClientRect().top;
    var eAmt = eTop / 100;
    var curTime = 0;
    while (curTime <= time) {
        window.setTimeout(SVS_B, curTime, eAmt, where);
        curTime += time / 100;
    }
}

function SVS_B(eAmt, where) {
    if(where == "center" || where == "")
        window.scrollBy(0, eAmt / 2);
    if (where == "top")
        window.scrollBy(0, eAmt);
}

And if you need horizontal scrolling:

function SmoothHorizontalScrolling(e, time, amount, start) {
    var eAmt = amount / 100;
    var curTime = 0;
    var scrollCounter = 0;
    while (curTime <= time) {
        window.setTimeout(SHS_B, curTime, e, scrollCounter, eAmt, start);
        curTime += time / 100;
        scrollCounter++;
    }
}

function SHS_B(e, sc, eAmt, start) {
    e.scrollLeft = (eAmt * sc) + start;
}

And an example call is:

SmoothVerticalScrolling(myelement, 275, "center");
Answer

Use smootscroll polyfill (solution for all browsers), easy applicable and lightweight dependency: https://github.com/iamdustan/smoothscroll

Once you install it via npm or yarn, add it to your main .js, .ts file (one which executes first)

import smoothscroll from 'smoothscroll-polyfill';
// or if linting/typescript complains
import * as smoothscroll from 'smoothscroll-polyfill';

// kick off the polyfill!
smoothscroll.polyfill();
Answer

The solution with the smoothest performance, especially if you want to incorporate easing is to use requestAnimationFrame:

const requestAnimationFrame = window.requestAnimationFrame ||
          window.mozRequestAnimationFrame ||
          window.webkitRequestAnimationFrame ||
          window.msRequestAnimationFrame;

const step = (timestamp) => {
  window.scrollBy(
    0,
    1, // or whatever INTEGER you want (this controls the speed)
  );

  requestAnimationFrame(step);
};


requestAnimationFrame(step);

if you want to later cancel the scroll, you need to have a reference to your requestAnimationFrame (do this everywhere you use requestAnimationFrame(step)):

this.myRequestAnimationFrame = requestAnimationFrame(step);

const cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;
cancelAnimationFrame(this.myRequestAnimationFrame);

Now what if you want to use easing with your scroll and take timeouts between scroll actions?

create an array of 60 elements (requestAnimationFrame usually calls 60 times per second. It's technically whatever the refresh rate of the browser is, but 60 is the most common number.) We are going to fill this array non-linearly then use those numbers to control how much to scroll at each step of requestAnimationFrame:

let easingPoints = new Array(60).fill(0)

choose an easing function. Let's say we're doing a cubic ease-out:

function easeCubicOut(t) {
    return --t * t * t + 1;
}

create a dummy array and fill it with data piped through the easing function. You'll see why we need this in a moment:

    // easing function will take care of decrementing t at each call (too lazy to test it at the moment. If it doesn't, just pass it a decrementing value at each call)
    let t = 60;
    const dummyPoints = new Array(60).fill(0).map(()=> easeCubicOut(t));
    const dummyPointsSum = dummyPoints.reduce((a, el) => {
                                a += el;
                               return a;
                           }, 0);

map easingPoints using the help of each dummyPoint ratio to dummyPointsSum:

    easingPoints = easingPoints.map((el, i) => {
        return Math.round(MY_SCROLL_DISTANCE * dummyPoints[i] / dummyPointsSum);
    });

in your scroll function, we'll make a few adjustments:

     const requestAnimationFrame = window.requestAnimationFrame ||
              window.mozRequestAnimationFrame ||
              window.webkitRequestAnimationFrame ||
              window.msRequestAnimationFrame;

     let i = 0;
     const step = (timestamp) => {
       window.scrollBy(
         0,
         easingPoints[i],
       );


        if (++i === 60) {
                i = 0;
                return setTimeout(() => {
                  this.myRequestAnimationFrame = requestAnimationFrame(step);
                }, YOUR_TIMEOUT_HERE);
        }
      };


      this.myRequestAnimationFrame = requestAnimationFrame(step);
Answer

A simple jQuery fix that works for Safari:

$('a[href*="#"]').not('[href="#"]').not('[href="#0"]').click(function (t) {
    if (location.pathname.replace(/^\//, "") == this.pathname.replace(/^\//, "") && location.hostname == this.hostname) {
        var e = $(this.hash);
        e = e.length ? e : $("[name=" + this.hash.slice(1) + "]"), e.length && (t.preventDefault(), $("html, body").animate({
            scrollTop: e.offset().top
        }, 600, function () {
            var t = $(e);
            if (t.focus(), t.is(":focus")) return !1;
            t.attr("tabindex", "-1"), t.focus()
        }))
    }
});

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us

©2020 All rights reserved.