Scroll to an element smoothly
Written byPhuoc Nguyen
Created
26 Apr, 2020
Category
Level 3 — Advanced
As mentioned in the Scroll to an element post, we can scroll to given element smoothly by passing
`behavior: 'smooth'`
:js
ele.scrollIntoView({ behavior: 'smooth' });
or applying the CSS property
`scroll-behavior`
to the target element:css
scroll-behavior: smooth;
Both methods aren't supported in IE and Safari, and don't allow to customize the animation.
This post introduces a smoothly scroll implementation which also allows us to customize the animation effect and duration. We'll demonstrate in a popular use case that user can jump between sections by clicking associated navigation button.
ResourceThe post doesn't mention how to add a navigation fixed on the left (or right) of the page. You can see a simple way to create a fixed navigation on the CSS Layout collection.
The navigation consists of some
`a`
elements:html
<a href="#section-1" class="trigger"></a>
<a href="#section-2" class="trigger"></a>
...
<div id="section-1">...</div>
<div id="section-2">...</div>
Clicking the link will scroll the page to a particular element that can be determined by the
`href`
attribute.js
const triggers = [].slice.call(document.querySelectorAll('.trigger'));
triggers.forEach(function (ele) {
ele.addEventListener('click', clickHandler);
});
The
`clickHandler`
function handles the `click`
event of a navigation element. It determintes the target section based on the `href`
attribute. Notice that we will scroll to the target section ourselves, hence the default action will be ignored:js
const clickHandler = function (e) {
// Prevent the default action
e.preventDefault();
// Get the `href` attribute
const href = e.target.getAttribute('href');
const id = href.substr(1);
const target = document.getElementById(id);
scrollToTarget(target);
};
Don't worry if you haven't seen the
`scrollToTarget`
function. As the name implies, the function will scroll the page to given `target`
.#Scroll to given target
It is the main part of the post. To scroll to given point, we can use
`window.scrollTo(0, y)`
where `y`
indicates the distance from the top of the page to the target.Good to knowIt's also possible to set`behavior: 'smooth'`
:js
window.scrollTo({ top, left, behavior: 'smooth' });The option isn't supported in IE and Safari.
What we're going to do is to move from the starting point to the ending point in given duration.
- The starting point is the current y-axis offset,
`window.pageYOffset`
- The ending point is the top distance of the target. It can be retrieved as
`target.getBoundingClientRect().top`
- The duration is a number of milliseconds. You can change it to a configurable option, but in this post, it's set as 800.
js
const duration = 800;
const scrollToTarget = function (target) {
const top = target.getBoundingClientRect().top;
const startPos = window.pageYOffset;
const diff = top;
let startTime = null;
let requestId;
const loop = function (currentTime) {
if (!startTime) {
startTime = currentTime;
}
// Elapsed time in miliseconds
const time = currentTime - startTime;
const percent = Math.min(time / duration, 1);
window.scrollTo(0, startPos + diff * percent);
if (time < duration) {
// Continue moving
requestId = window.requestAnimationFrame(loop);
} else {
window.cancelAnimationFrame(requestId);
}
};
requestId = window.requestAnimationFrame(loop);
};
As you see, we tell the browser to execute the
`loop`
function before the next paint happens. At the first time, `startTime`
will be initialized as the current timestamp (`currentTime`
).We then calculate how many milliseconds has gone:
js
const time = currentTime - startTime;
Based on the elapsed time and the duration, it's so easy to calculate the number of percentages we have been moving, and scroll to that position:
js
// `percent` is in the range of 0 and 1
const percent = Math.min(time / duration, 1);
window.scrollTo(0, startPos + diff * percent);
Finally, if there's remaining time, we continue looping. Otherwise, we cancel the last request:
js
if (time < duration) {
// Continue moving
requestId = window.requestAnimationFrame(loop);
} else {
window.cancelAnimationFrame(requestId);
}
Good practiceIt's common to think of using`setTimeout()`
or`setInterval()`
when we need to move between given points in the given duration. But it's recommended to use requestAnimationFrame which gives better performance animation.
#Customize the animation
Currently, we move to the target equally per millisecond. We move the same distance every milliseconds.
If you want, you can replace the current linear movement with other easing functions. Look at this website to imagine how each easing produces different animations.
The code below uses the
`easeInQuad`
animation:js
const easeInQuad = function(t) {
return t * t;
};
const loop = function(currentTime) {
...
const percent = Math.min(time / duration, 1);
window.scrollTo(0, startPos + diff * easeInQuad(percent));
});
In the following demo, try to move between sections by clicking an circle on the left.
#Demo
#See also
Questions? 🙋
Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Recent posts ⚡
Newsletter 🔔
If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn