Featured image of post Back-to-Top Button for Hugo Stack theme

Back-to-Top Button for Hugo Stack theme

This post serves as a personal note of my journey building this site. The code and reflections here are primarily for learning and may not always be correct or production-ready.

When experimenting with my blog, I thought a floating Table of Contents (TOC) button for mobile devices would be a great idea (especially for post with massive contents) . However, after searching online, I couldn’t find much information on an existing implementation. So, I decided to break it down into smaller steps, starting with a simpler feature: building a back-to-top button.

Here’s how I did it, step by step.

Adding the HTML for the Button

I’m using the Stack theme, which has a convenient custom.html file located in layouts/footer. This file is perfect for adding custom features without modifying the theme’s core files.

Here’s the code I added:

<div id="back-to-top">
  <button>
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" 
      viewBox="0 0 24 24" fill="none" stroke="currentColor"
      stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <polyline points="18 15 12 9 6 15"></polyline>
    </svg>
  </button>
</div>

Styling the button with CSS

To make the button visually appealing and adapt to both light and dark themes, I updated scss/partials/footer.scss. Here’s the CSS:

#back-to-top {
    position: fixed;
    bottom: 20px;
    right: 20px;
    z-index: 1000;
    display: none;
}

#back-to-top button {
    padding: 8px;
    border: none;
    border-radius: 8px;
    background-color: var(--back-to-top-bg);
    box-shadow: 0 3px 5px var(--back-to-top-shadow);
    cursor: pointer;
    width: 36px;
    height: 36px;
    display: flex;
    justify-content: center;
    align-items: center;
    transition: all 0.3s ease;
}

#back-to-top button:hover {
    background-color: var(--back-to-top-bg-hover);
    box-shadow: 0 5px 8px var(--back-to-top-shadow-hover);
    transform: translateY(-2px);
}

#back-to-top button svg {
    width: 16px;
    height: 16px;
    // fill: var(--back-to-top-arrow);
    stroke: var(--back-to-top-arrow);
    transition: stroke 0.3s ease;
}

#back-to-top button:hover svg {
    // fill: var(--back-to-top-arrow);
    stroke: var(--back-to-top-arrow);
}

I updated variables.scss to define the color variables for light and dark themes, as well as for a good separation:

/* 
*   Footer back to top style
*/
:root {
    --back-to-top-bg: #f9f9fc;
    --back-to-top-bg-hover: #ececf6;
    --back-to-top-arrow: #2c3e50;
    --back-to-top-shadow: rgba(0, 0, 0, 0.1);
    --back-to-top-shadow-hover: rgba(0, 0, 0, 0.15);
}

[data-scheme="dark"] {
    --back-to-top-bg: #424242;
    --back-to-top-bg-hover: #383838;
    --back-to-top-arrow: rgba(255, 255, 255, 0.7);
    --back-to-top-shadow: rgba(0, 0, 0, 0.3);
    --back-to-top-shadow-hover: rgba(0, 0, 0, 0.5);
}

Bringing the button to life with JS

The next step was making the button functional. I wanted it to:

  1. Show or hide based on scroll position (where you can modify the value).
  2. Scroll smoothly to the top when clicked.

To achieve this, I added the following JavaScript in footer/components/script.html:

<script>
    document.addEventListener("DOMContentLoaded", function () {
        const backToTop = document.getElementById("back-to-top");

        // Show or hide the button on scroll
        window.addEventListener("scroll", function () {
            if (window.scrollY > 400) {
                backToTop.style.display = "block";
            } else {
                backToTop.style.display = "none";
            }
        });

        // Scroll to the top
        backToTop.addEventListener("click", function () {
            window.scrollTo({ top: 0, behavior: "smooth" });
        });
    });
</script>

Reflections and Takeaways

One interesting takeaway from this practice was learning about the advantages of using addEventListener over inline event handlers like onscroll or onclick. addEventListener

  1. supports Multiple Event Listeners, meaning in the future we can add other functions to the same event without overwriting existing handlers.
  2. allows cleaner code by separating event registration from HTML and other JavaScript logic.

Here the function is called twice, both have distinct roles.

The outer addEventListener("DOMContentLoaded") ensures the script runs after the DOM is fully loaded and parsed. This is crucial because document.getElementById("back-to-top") might return null if the DOM hasn’t been built yet. Without this, subsequent code would fail.

The inner addEventListener("scroll") listens for the user scrolling the page. It dynamically toggles the button’s visibility based on the scroll position, ensuring it appears only when needed.

While searching realization online, I found some useful links for reference.