How to Make an Unobtrusive Scroll-to-Top Button

Avatar of Marcel Rojas
Marcel Rojas on

A button to return to the top of the page allows the user to quickly return to the top of the page without making too much effort. This can be very useful when the page has a lot of content or which happens, for example, on one page websites, when infinite scrolling is used, or on mobile devices where different screen sizes can cause the content to scroll extend.

Those buttons usually float in the bottom corner of sites and then take you back to the top of the page when clicked. They are pretty easy to create with JavaScript. But visually, we are looking for it to be non-obtrusive while still being a large enough target to tap or click. Let’s look at a few ways we can do this, starting simple, then improving things as we go.

Option 1: Keep it simple

First, we select the button in JavaScript.

var scrollToTopBtn = document.getElementById("scrollToTopBtn")

Now document.documentElement returns the root element of the document. We need it to get the offset values. So, next let’s save it in a variable called rootElement — that way it’s easier to call in the code.

var rootElement = document.documentElement

We’ll add a click event listener to the button:

function scrollToTop {
  // scroll to top logic
}

scrollToTopBtn.addEventListener("click", scrollToTop)

Then, inside the scrollToTop function, we will make it scroll to the top of the screen with the scrollTo method.

function scrollToTop() {
  // Scroll to top logic
  rootElement.scrollTo({
    top: 0,
    behavior: "smooth"
  })
}

We can style the button up a bit as well:

#scrollToTopBtn {
  background-color: black;
  border: none;
  border-radius: 50%;
  color: white;
  cursor: pointer;
  font-size: 16px;
  line-height: 48px;
  width: 48px;
}

Now we’re able to drop the button somewhere down the page, say, the footer:

<footer>
  <!-- Scroll to top button -->
  <button id="scrollToTopBtn">☝️</button>
</footer>

And we get this:

Option 2: Detecting the scroll position

We can detect scrolling with a scroll event listener.

function handleScroll() {
  // Do something on scroll
}
document.addEventListener("scroll", handleScroll)

The handleScroll function will be called every time the user scrolls. Now we need the total number of pixels we can scroll.

  • scrollHeight gives the height of an element, including the part not visible due to overflow.
  • clientHeight gives the inner height of an element in pixels, which is the height of the visible part.

If we subtract scrollHeight by clientHeight, we get the total amount of pixels that we can scroll:

var scrollTotal = rootElement.scrollHeight - rootElement.clientHeight

Now we have a variable called scrollTotal that represents the maximum number of pixels that can be scrolled vertically. By dividing the amount scrolled by the total amount of pixels we can scroll, we get a ratio between 0 and 1. Playing with this ratio, we can easily toggle the button on and off.

For example, we will add a condition that shows the scroll-to-top button when the user has scrolled 80%, (or a ratio of 0.80) down the total height of the page. 80% is an arbitrary number. Basically, the closer we get to 1, the more the user has to scroll before seeing the button.

Here’s the JavaScript:

var rootElement = document.documentElement


function handleScroll() {
  // Do something on scroll
  var scrollTotal = rootElement.scrollHeight - rootElement.clientHeight
  if ((rootElement.scrollTop / scrollTotal ) > 0.80 ) {
    // Show button
    scrollToTopBtn.classList.add("showBtn")
  } else {
    // Hide button
    scrollToTopBtn.classList.remove("showBtn")
  }
}


document.addEventListener("scroll", handleScroll)

We’re going to want some CSS to position the button correctly when it comes into view:

.scrollToTopBtn {
  /* same general styles as before */
  
  /* place it at the bottom-right corner */
  position: fixed;
  bottom: 30px;
  right: 30px;


  /* keep it at the top of everything else */
  z-index: 100;


  /* hide with opacity */
  opacity: 0;


  /* also add a translate effect */
  transform: translateY(100px);


  /* and a transition */
  transition: all .5s ease
}


.showBtn {
  opacity: 1;
  transform: translateY(0)
}

With that, the button appears when the user gets 80% down the page and then hides when it’s higher than that.

This seems like a grand option, and setting an event listener to do this is pretty easy. But the performance overhead can be costly since we’re always checking the current scroll position.

There’s another option that takes care of this…

Option 3: Intersection Observer

The Intersection Observer API is an excellent solution to the above problem. It’s a fairly recent browser API that lets developers hand most of these tasks off to the browser, in a way that is more optimized. Travis Almand wrote up a thorough explanation of how it works. Here’s how MDN defines it:

The Intersection Observer API provides a way to asynchronously observe changes for the intersection of a target element with an ancestor element or with a top-level document’s viewport.

Pretty neat! That means the button can be our target element:

// We select the element we want to target
var target = document.querySelector("footer");

We then write a callback function that does something when our element becomes “intersects” with the viewport — which is a fancy way of saying when it comes into view.

And once the footer enters or leaves the viewport, all we really want to do is add or remove a class. The callback receives an array of entries as a parameter.

function callback(entries, observer) {
  // The callback will return an array of entries, even if you are only observing a single item
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      // Show button
      scrollToTopBtn.classList.add('showBtn')
    } else {
      // Hide button
      scrollToTopBtn.classList.remove('showBtn')
    }
  });
}

We need to create a new IntersectionObserver instance and pass it the callback function we just wrote.

let observer = new IntersectionObserver(callback);

Finally, we tell the observer to start watching (err, observing) the target element that was selected above for when it intersects with the viewport:

observer.observe(target);

And what about smooth scrolling?

Of course it’s possible! In fact, Chris showed us how it can be done with CSS back in 2019:

<html id="top">
  <body>
     <!-- the entire document -->
     <a href="#top">Jump to top of page</a>
  </body>
</html>
html {
  scroll-behavior: smooth;
}

There’s a little more nuance to there, like accessibility enhancements that Chris also covers in the post. The point is that CSS is gaining new powers that can accomplish things that we used to use JavaScript for.


There you have it! We started with a pretty simple idea. We enhanced it by displaying and hiding the button based on the user’s scroll position. Then we improved the performance by implementing the Intersection Observer API instead of watching the current scroll position. And, finally, we saw how CSS can be used for smooth scrolling. All together, we get a scroll-to-top button that is easy to see and use, while not blocking other elements on the page.