Creating My First Web Component: The <back-to-top> Button

Murtuzaali Surti
Murtuzaali Surti

• 5 min read

Updated On

I came across the concept of web components, when I saw the word "WebC" in one of the Zach Leatherman's blog posts. Then, I came to know that:

WebC is a framework-independent standalone HTML compiler for generating markup for web components. - zachleat.com

That ignited a spark of curiosity within me and I started reading and researching more about web components. Eventually, after realizing you can build and define custom HTML elements, I decided to build at least one of my own.

It's alright if you didn't already know about custom elements or web components until you came here (and I think for most entry-level developers, that is the case), but I assure you that you will find the world of web components fascinating.

If you want to learn more about the basics of web components and custom elements, I wrote a post explaining what they are.

You might have seen "back to top" buttons/links on some web pages(especially those which contain long form articles) which take you to the top of the page on a click. I took the functionality of those buttons and moved it in a re-usable web component. In this post, I will walk you through the process of how I built a <back-to-top> web component.

Find it here 📦

Building v1.0

Custom elements can be defined in two ways - by extending an existing HTML element class such as a button (HTMLButtonElement) or by extending the generic HTMLElement class. If you go the first route, you get all of the properties of a button element when you call the super() method in the constructor but you lose the ability to attach a shadow DOM to that custom element since shadow DOM is only supported by certain elements.

Despite the limitations, I decided to go with a customized built-in custom element a.k.a the element which extends a specific class of the element to be used - in my case, a button.

class BackToTop extends HTMLButtonElement {
    constructor() {
        super();
        // properties
    }

    // inspired by David Darnes' component template - https://github.com/daviddarnes/component-template
    static register(tagName, extendsElement) {
        if ("customElements" in window) {
            if (extendsElement) {
                customElements.define(tagName || "back-to-top", BackToTop, { extends: extendsElement }); // you must specify extends option while defining a customized built-in element
                return;
            }
            customElements.define(tagName || "back-to-top", BackToTop);
        }
    }

    connectedCallback() {
        // code
    }
}

BackToTop.register("back-to-top", "button");

For event throttling, lodash seemed to be the best option because it allows you to create a custom build specific to your functionality. If you only want the throttle or debounce module, then you can get it by using:

lodash include=throttle,debounce -p

The lodash custom build file gets imported in the web component file by a build step I perform using esbuild.

import "./lodash.custom.min.js"

class BackToTop extends HTMLButtonElement {
    constructor() {
        super();
    }
    // ...
}

Lodash is used to throttle the position calculation function in order to show/hide the "back to top" button.

handleThrottle = _.throttle(() => {
    let prevScrollPos =
        document.documentElement.scrollTop ||
        window.scrollY ||
        document.body.scrollTop;

    this.currentScrollPos <= prevScrollPos
        ? this.style = this.#hidden
        : this.style = this.#show;

    this.currentScrollPos = prevScrollPos;

    this.currentScrollPos === 0 &&
        (this.style = this.#hidden);
}, 400)

The connectedCallback method is executed once the web component class is instantiated, but there's also disconnectedCallback which fires when the component is removed from the document. So, make sure to remove any listeners you have attached to the element inside this method.

disconnectedCallback() {
    window.removeEventListener("scroll", this.handleThrottle);
    this.removeEventListener("click", this.handleClick);
}

You can find the code for v1.0 on npm.

Building v2.0

After publishing the first version, I got wonderful responses and ideas from the community. One of them was to use an autonomous custom element (extending the generic HTMLElement class) because Safari doesn't yet support the is attribute required for customized built-in elements to work. This forced me to rewrite the component as an autonomous custom element by using a wrapper around the button element.

class BackToTop extends HTMLElement {
    constructor() {
        super();
        // properties
    }

    static register(tagName) {
        if ("customElements" in window) {
            customElements.define(tagName || "back-to-top", BackToTop);
        }
    }

    connectedCallback() {
        // code
    }
}

BackToTop.register();

Previously, the throttling rate was hardcoded inside the component, but with v2.0, the throttle attribute was introduced to let users input a custom throttle rate measured in milliseconds. To handle attributes and their updates, you must define the attributes you want to watch in an observedAttributes static property. You can listen to the updates on those defined attributes using the attributeChangedCallback method.

You can store the value of the attribute in a class property. It's important to keep in sync the property and the attribute by setting the value of the property whenever the attribute value is modified.

class BackToTop extends HTMLElement {
    constructor() {
        super();
        // ...
        this.throttleRate = 400; // milliseconds
    }

    // defining which attributes to observe
    static get observedAttributes() {
        return ["throttle"];
    }

    // getter
    get getThrottleRate() {
        return this.throttleRate;
    }

    // setter
    set setThrottleRate(value) {
        this.throttleRate = Number(value);
    }

    // observing the "throttle" attribute
    attributeChangedCallback(name, oldVal, newVal) {
        name === "throttle" && (this.setThrottleRate = newVal) && (this.handleThrottle = this.throttledFunction(this.getThrottleRate));
    }

    // ...
}

Those were the main points I wanted to cover in this post which were crucial to building this component. Obviously, this is not the entirety of code. Check out the entire code on GitHub and see the web component in action below:

See the pen (@seekertruth) on CodePen.

One Last Thing

With all of the refactoring, the component still relies on javascript to render the button because of this:

// ...
connectedCallback() {
    this.append(document.createElement("button"));
    // ...
}
// ...

This is why, this can't be called an HTML web component because it doesn't fallback to anything when javascript can't execute. Now that I know about the approach of HTML web components, they have started making more sense to me as they would fallback to basic HTML behavior in absence of javascript execution. That gives me the motivation to build v3.0.

UPDATE: v3.0 is here and it now supports a fallback anchor link and customizable button content. You can update your component definition as shown below:

<back-to-top throttle="350">
  <a href="#" style="position: fixed; left: 1rem; bottom: 2rem;">back-to-top</a>
  <template>
     button content here
  </template>
</back-to-top>

React 19 - A Brief Look At Form Handling

Previous

Running PostgreSQL using Docker

Next