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 the expected behaviour? Is there a way to make it work nicely?
-
1Pretty late response - but behaviors will not work with Safari or Edge. You will have to implement your own system or use a library. – George Daniel Sep 26 '18 at 15:50
-
1There is an official bug in the safari bug tracker for it: https://bugs.webkit.org/show_bug.cgi?id=188043 and https://bugs.webkit.org/show_bug.cgi?id=189907 – jantimon Aug 29 '19 at 14:18
9 Answers
Use smoothscroll 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();

- 2,094
- 1
- 10
- 22

- 13,162
- 13
- 64
- 86
-
-
-
it is not working in index or in app.js is it tested in react all ? – Richardson Dec 15 '21 at 00:20
-
2This worked for me, inserted in `index.js` file. I always think it's best to put this configurations in `index.js`, since App.js is a component. – thismarcoantonio Jan 28 '22 at 14:41
-
Whatever, as long as this criterion is met which is included in the answer: "one which executes first" – EugenSunic Jan 28 '22 at 14:50
-
Not sure if anyone else has experienced this, but this package interferes with the scroll functionality I've implemented myself. Just importing it breaks my navbar that should have a style of fixed after a vertical scroll value has met. Just thought I'd mention it. I ended up going with this cdn instead `` – Nunchuk Mar 11 '22 at 05:15
-
@Nunchuk I'd suggest either use your functionality or the one I proposed. Why would you use 2 implementations. – EugenSunic Mar 11 '22 at 12:18
-
1@EugenSunic Using my functionality works quite well, and the only thing I'm missing is smooth scrolling for safari. I'll pass on your suggestion and use the cdn. It works great. – Nunchuk Mar 11 '22 at 19:58
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");

- 13,162
- 13
- 64
- 86

- 570
- 6
- 9
-
6@George Daniel, kudos for native JS solution, although you could improve your answer by adding some inline comments to the code. – Oksana Romaniv Oct 15 '19 at 07:43
-
2Window.requestAnimationFrame() should be used instead of timeouts, because of performance optimalisation and it runs only when visible.. etc.. https://blog.teamtreehouse.com/efficient-animations-with-requestanimationframe – Luckylooke Oct 25 '19 at 08:54
-
2Thank you for this @George Daniel, I made a small pen trying to describe the process of your function with comments here https://codepen.io/gfcf14/pen/qBEMWJe – gfcf14 Jan 16 '20 at 15:53
-
@Mathi What are you trying to do? Put up a codepen of what's not working. This is quite an old solution but should still work. – George Daniel Sep 16 '20 at 22:05
-
-
@Bonsaï You just need to simply copy and paste my function into your javascript library/area. Then, just make a function call (like in the example shown) to the target element. – George Daniel Jan 19 '21 at 00:49
-
1@GeorgeDaniel I do the same but it doesn't work. I don't get any response – Ulvi Mar 17 '21 at 16:51
For a more comprehensive list of methods for smooth scrolling, see my answer here.
window.requestAnimationFrame
can be used to perform smooth scrolling in an exact amount of time.
For smooth vertical scrolling, the following function can be used. Note that horizontal scrolling can be done in much the same manner.
/*
@param time: the exact amount of time the scrolling will take (in milliseconds)
@param pos: the y-position to scroll to (in pixels)
*/
function scrollToSmoothly(pos, time) {
var currentPos = window.pageYOffset;
var start = null;
if(time == null) time = 500;
pos = +pos, time = +time;
window.requestAnimationFrame(function step(currentTime) {
start = !start ? currentTime : start;
var progress = currentTime - start;
if (currentPos < pos) {
window.scrollTo(0, ((pos - currentPos) * progress / time) + currentPos);
} else {
window.scrollTo(0, currentPos - ((currentPos - pos) * progress / time));
}
if (progress < time) {
window.requestAnimationFrame(step);
} else {
window.scrollTo(0, pos);
}
});
}
Demo:
/*
@param time: the exact amount of time the scrolling will take (in milliseconds)
@param pos: the y-position to scroll to (in pixels)
*/
function scrollToSmoothly(pos, time) {
var currentPos = window.pageYOffset;
var start = null;
if(time == null) time = 500;
pos = +pos, time = +time;
window.requestAnimationFrame(function step(currentTime) {
start = !start ? currentTime : start;
var progress = currentTime - start;
if (currentPos < pos) {
window.scrollTo(0, ((pos - currentPos) * progress / time) + currentPos);
} else {
window.scrollTo(0, currentPos - ((currentPos - pos) * progress / time));
}
if (progress < time) {
window.requestAnimationFrame(step);
} else {
window.scrollTo(0, pos);
}
});
}
document.querySelector('button').addEventListener('click', function(e){
scrollToSmoothly(500, 1500);
});
html, body {
height: 1000px;
}
<button>Scroll to y-position 500px in 1500ms</button>
For more complex cases, the SmoothScroll.js library can be used, which handles smooth scrolling both vertically and horizontally, scrolling inside other container elements, different easing behaviors, scrolling relatively from the current position, and more. It also supports most browsers that do not have native smooth scrolling.
var easings = document.getElementById("easings");
for(var key in smoothScroll.easing){
if(smoothScroll.easing.hasOwnProperty(key)){
var option = document.createElement('option');
option.text = option.value = key;
easings.add(option);
}
}
document.getElementById('to-bottom').addEventListener('click', function(e){
smoothScroll({yPos: 'end', easing: easings.value, duration: 2000});
});
document.getElementById('to-top').addEventListener('click', function(e){
smoothScroll({yPos: 'start', easing: easings.value, duration: 2000});
});
<script src="https://cdn.jsdelivr.net/gh/LieutenantPeacock/SmoothScroll@1.2.0/src/smoothscroll.min.js" integrity="sha384-UdJHYJK9eDBy7vML0TvJGlCpvrJhCuOPGTc7tHbA+jHEgCgjWpPbmMvmd/2bzdXU" crossorigin="anonymous"></script>
<!-- Taken from one of the library examples -->
Easing: <select id="easings"></select>
<button id="to-bottom">Scroll To Bottom</button>
<br>
<button id="to-top" style="margin-top: 5000px;">Scroll To Top</button>

- 76,500
- 11
- 62
- 80
-
-
-
Thank you! I reached smoother scrolling by putting this line `var currentPos = window.pageYOffset;`Below `window.requestAnimationFrame` – unx Dec 07 '22 at 10:45
The workarounds above all make up for the lack of Safari support for behaviors.
It's still necessary to detect when a workaround is needed.
This little function will detect if smooth scrolling is supported by the browser. It returns false on Safari, true on Chrome and Firefox:
// returns true if browser supports smooth scrolling
const supportsSmoothScrolling = () => {
const body = document.body;
const scrollSave = body.style.scrollBehavior;
body.style.scrollBehavior = 'smooth';
const hasSmooth = getComputedStyle(body).scrollBehavior === 'smooth';
body.style.scrollBehavior = scrollSave;
return hasSmooth;
};
const pre = document.querySelector('pre');
// returns true if browser supports smooth scrolling
const supportsSmoothScrolling = () => {
const body = document.body;
const scrollSave = body.style.scrollBehavior;
body.style.scrollBehavior = 'smooth';
const hasSmooth = getComputedStyle(body).scrollBehavior === 'smooth';
body.style.scrollBehavior = scrollSave;
return hasSmooth;
};
const supported = supportsSmoothScrolling();
pre.innerHTML = `supported: ${ (supported) ? 'true' : 'false'}`;
<h3>
Testing if 'scrollBehavior smooth' is supported
</h3>
<pre></pre>
Update
Test of Safari Technology Preview, Release 139 (Safari 15.4) shows support for scrollBehavior smooth
, so we may expect to see support in 15.4.

- 6,771
- 1
- 21
- 27
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);

- 690
- 11
- 12
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()
}))
}
});

- 99
- 1
- 5
-
@conner cowling: Your code (I changed it a bit) helped me when content is lazy-loaded, resulting in content shifts breaking the final scroll offset position. Many thanks!!! – AndreasRu Jan 19 '22 at 09:57
Combining the answers of George Daniel and terrymorse, the following can be used for all the browser's support using native JavaScript.
As, Chrome, Firefox supports CSS, scroll-behavior: smooth;
for the browsers which don't support this property, we can add below.
HTML:
<a onclick="scrollToSection(event)" href="#section">
Redirect On section
</a>
<section id="section">
Section Content
</section>
CSS:
body {
scroll-behavior: smooth;
}
JavaScript:
function scrollToSection(event) {
if (supportsSmoothScrolling()) {
return;
}
event.preventDefault();
const scrollToElem = document.getElementById("section");
SmoothVerticalScrolling(scrollToElem, 300, "top");
}
function supportsSmoothScrolling() {
const body = document.body;
const scrollSave = body.style.scrollBehavior;
body.style.scrollBehavior = 'smooth';
const hasSmooth = getComputedStyle(body).scrollBehavior === 'smooth';
body.style.scrollBehavior = scrollSave;
return hasSmooth;
};
function SmoothVerticalScrolling(element, time, position) {
var eTop = element.getBoundingClientRect().top;
var eAmt = eTop / 100;
var curTime = 0;
while (curTime <= time) {
window.setTimeout(SVS_B, curTime, eAmt, position);
curTime += time / 100;
}
}
function SVS_B(eAmt, position) {
if (position == "center" || position == "")
window.scrollBy(0, eAmt / 2);
if (position == "top")
window.scrollBy(0, eAmt);
}

- 4,602
- 4
- 22
- 36
-
Like all of the other answers, this doesn't work on Safari 14.1.2 (MacOS 11.5.1) – BSUK Aug 08 '21 at 22:04
-
-
I'm getting this error when clicking a link: TypeError: null is not an object (evaluating 'element.getBoundingClientRect') – BSUK Aug 10 '21 at 14:41
-
1@BSUK: I suspect that, here `const scrollToElem = document.getElementById("section");`, scrollToElem is null in your case. Please make sure you have an element with id="section", to which we wanna scroll. OR make sure you add a proper id selector so that scrollToElem won't be null. – Aniruddha Shevle Aug 11 '21 at 02:11
-
1Many thanks for your help! It now works if I hard code the element id, but obviously it has to work dynamically based on whichever anchor link is clicked on (like the default Smooth scrolling behaviour). I guess I need to brush up on my javascript and develop this function a bit. Super annoying that Safari doesn't just support this CSS behaviour out of the box. – BSUK Aug 11 '21 at 12:07
-
One possible generalisation is to add the id on the link and pass it to the function, as ``. Then, the function would receive it and use it, instead of the hardcoded "section" `function scrollToSection(event,id) { ... const scrollToElem = document.getElementById(id)...}` – Filipe Jan 31 '22 at 14:42
-
@Filipe: You can definitely make it more dynamic. This answer is just for basic smooth scroll working on all the browsers! – Aniruddha Shevle Feb 01 '22 at 01:15
Another possible solution with an "ease-out" effect.
Inspired by some of the answers given earlier,
a key difference is in using "pace" instead of specifying a duration, I found that calculating the length of each step based on a fixed pace creates a smooth "ease-out" effect as the number of steps increases as the scroll approaches the destination point.
Hopefully the code below is easy to understand.
function smoothScrollTo(destination) {
//check if browser supports smooth scroll
if (window.CSS.supports('scroll-behavior', 'smooth')) {
window.scrollTo({ top: destination, behavior: 'smooth' });
} else {
const pace = 200;
let prevTimestamp = performance.now();
let currentPos = window.scrollY;
// @param: timestamp is a "DOMHightResTimeStamp", check on MDN
function step(timestamp) {
let remainingDistance = currentPos < destination ? destination - currentPos : currentPos - destination;
let stepDuration = timestamp - prevTimestamp;
let numOfSteps = pace / stepDuration;
let stepLength = remainingDistance / numOfSteps;
currentPos = currentPos < destination ? currentPos + stepLength : currentPos - stepLength;
window.scrollTo({ top: currentPos });
prevTimestamp = timestamp;
if (Math.floor(remainingDistance) >= 1) window.requestAnimationFrame(step);
}
window.requestAnimationFrame(step);
}
}
This is my first contribution on SO after years of benefiting from this great community. Constructive criticism is highly appreciated.

- 31
- 5
Thanks to T.Dayya, I had combined few answers on that topic and here is ts module with extension function scrollSmoothIntoView.
export default {}
declare global {
interface Element {
scrollSmoothIntoView(): void;
}
}
Element.prototype.scrollSmoothIntoView = function()
{
const t = 45;
const tstep = 6.425/t;
const dummyPoints = new Array(t).fill(0).map((t, i) => circ(i * tstep));
const dummyPointsSum = dummyPoints.reduce((a, el) => { a += el; return a;}, 0);
const _window: any = window;
const _elem: any = getScrollParent(this);
const scroll_distance: any = (this as any).offsetTop - (!_elem.parentElement ? _window.scrollY : 0);
let easingPoints = new Array(t).fill(0)
easingPoints = easingPoints.map((el, i) => {
return Math.round(scroll_distance * dummyPoints[i] / dummyPointsSum);
});
const requestAnimationFrame = _window.requestAnimationFrame ||
_window.mozRequestAnimationFrame ||
_window.webkitRequestAnimationFrame ||
_window.msRequestAnimationFrame;
let i = 0;
const step = (timestamp:any) => {
_elem.scrollBy(0, easingPoints[i]);
if (++i < t)
setTimeout(() => { requestAnimationFrame(step) }, 2);
};
window.requestAnimationFrame(()=>requestAnimationFrame(step));
}
function getScrollParent(element: any, includeHidden?: any):any {
var style = getComputedStyle(element);
var excludeStaticParent = style.position === "absolute";
var overflowRegex = includeHidden ? /(auto|scroll|hidden)/ : /(auto|scroll)/;
if (style.position === "fixed") return document.body;
for (var parent = element; (parent = parent.parentElement);) {
style = getComputedStyle(parent);
if (excludeStaticParent && style.position === "static") {
continue;
}
if (overflowRegex.test(style.overflow + style.overflowY + style.overflowX)) return parent;
}
return document.body;
}
function circ(t:any) {
return 1+Math.cos(3+t);
}
Using html_element.scrollSmoothIntoView().

- 469
- 5
- 9