Building a simple Back to Top button trigger with CSS and JavaScript

I’ve been trying to decouple my projects from dependencies and libraries as much as I can lately. It not only gives me a good opportunity to exercise some skills I have been neglecting but having the extra control over bugs and errors is also a welcome addition.

My latest adventure was the need to create a Back to Top button that would only show up after a specific element was on the page, and would then disappear when scrolled past a boundary container. Here is the basic acceptance criteria:

  • A top container triggers the button visibility state to true when intersecting the viewport
  • A bottom container removes the visible state from the button
  • Vanilla JS only
  • Back to Top button has a subtle opacity transition when changing states
  • Back to Top button cannot be clickable when hidden

With these basic conditions given, let’s stretch our coding muscles and bootstrap this solution!

Understanding the problem

Considering our markup to be composed of a Back to Top Button #back-to-top, a top container #section-top, and a bottom container #section-bottom, let’s start by storing pointer references to these DOM Elements:

    const btn = document.querySelector("#back-to-top");
    const topSection = document.querySelector("#section-top");
    const bottomSection = document.querySelector("#section-bottom");

Our first step is finding where both our section elements will intersect the page scroll flow. It seems like a simple task as you can state it as just find their scroll position value relative to the vertical space available on the page!. Well, this is the solution itself, but JavasScript doesn’t provide a specific method to find these values straight away. Instead, it provides a lot of helper methods available at the object level to deal with these types of calculations.

     const topScrollThreshold =  topSection.getBoundingClientRect().top + window.pageYOffset

The getBoundingClientRect method can be called from a DOM element (in this case, the topSection) and will return a DOMRect object with properties that describe its size and its position relative to the viewport:

Safari's new tab

One important thing to consider is that the position properties (top, left, and so forth) returned by this method are relative to the viewport and are not aware of our total page height, which is fundamental for us to understand when the Back to Top button should show up. To work around this issue, we add the top value of our element to the current scroll position by accessing window.PageYOffset. Property names are also important here: window.pageYOffset is the same as window.scrollY, but it has native support on IE.

A more in-depth look of the values getBoundingClientRect can also teach us that padding and border-width are added to the total width/height values if we’re using the standard box model:

Safari's new tab

Making use of box-sizing: border-box is a way to get rid of this behaviour, but for our case just being mindful of this rule is enough.

Observing if we’re already past the containers

As a way of streamlining this process, we’re going to add a conditional logic to our topScrollThreshold so it returns a boolean. We’re now planning on making these variables live inside a scroll window event, so every time users scroll the variables will be checked and return a new set of values:

  window.addEventListener("scroll", function (e) {
   const topScrollThreshold = topSection.getBoundingClientRect().top + window.pageYOffset < window.pageYOffset;
  }

We’re adding our top container .top property to pageYOffset to find its position relative to the entire page scroll, and then checking if that value is smaller than the page scroll position (pageYOffset). For the bottom container the logic has a little caveat:

  const bottomScrollThreshold = btn.getBoundingClientRect().top + window.pageYOffset > bottomSection.getBoundingClientRect().top + window.pageYOffset;

Back to our requirements:

  • A bottom container removes the visible state from the button

There is a chance that nothing will live below the Bottom container and it will never reach the top of the page. According to MDN:

The read-only Window property pageYOffset (…) returns the number of pixels the document is currently scrolled along the vertical axis (that is, up or down) with a value of 0.0, indicating that the top edge of the Document is currently aligned with the top edge of the window’s content area.

That’s the issue with measuring its position alone as we did to the Top container. Checking its intersection with the Back to Top button is a more fail-proof choice in this case.

A little CSS

The extra CSS to make this button work is pretty simple: in its default state (hidden), no pointer-events will be observed and the opacity is set to zero. We’re also positioning it as fixed relative to the viewport:

.back-to-top {
  position: fixed;
  bottom: 5vh;
  right: 5vw;
  opacity: 0;
  transition: opacity 0.5s;
  pointer-events: none;
}

.back-to-top.visible {
    opacity: 1;
    pointer-events: all;
}

The .visible class describes its active state when it’s clickable and fully opaque.

Putting it all together

The final steps are adding more simple logic to trigger our button state based on the final outcome of the threshold checkers:

//setting visibility based on scroll pos
    if (topScrollThreshold) {
      btn.classList.add("visible");
    } else if (btn.classList.contains("visible")) {
      btn.classList.remove("visible");
    }

    if (bottomScrollThreshold) {
      btn.classList.remove("visible");
    }

You can see the live codepenhere

.